Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72c75fd1a9 | |||
| 7c31ec07ba | |||
| 6a4fc86387 | |||
| 8dc27ff8a9 | |||
| 29956665ac | |||
| f65bf2ef5c | |||
| aafdeba93e | |||
| acee97a579 | |||
| 38b8e36fab | |||
| 3ad368f935 |
@@ -0,0 +1,241 @@
|
||||
---
|
||||
name: prd
|
||||
description: "Generate a Product Requirements Document (PRD) for a new feature. Use when planning a feature, starting a new project, or when asked to create a PRD. Triggers on: create a prd, write prd for, plan this feature, requirements for, spec out."
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# PRD Generator
|
||||
|
||||
Create detailed Product Requirements Documents that are clear, actionable, and suitable for implementation.
|
||||
|
||||
---
|
||||
|
||||
## The Job
|
||||
|
||||
1. Receive a feature description from the user
|
||||
2. Ask 3-5 essential clarifying questions (with lettered options)
|
||||
3. Generate a structured PRD based on answers
|
||||
4. Save to `tasks/prd-[feature-name].md`
|
||||
|
||||
**Important:** Do NOT start implementing. Just create the PRD.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Clarifying Questions
|
||||
|
||||
Ask only critical questions where the initial prompt is ambiguous. Focus on:
|
||||
|
||||
- **Problem/Goal:** What problem does this solve?
|
||||
- **Core Functionality:** What are the key actions?
|
||||
- **Scope/Boundaries:** What should it NOT do?
|
||||
- **Success Criteria:** How do we know it's done?
|
||||
|
||||
### Format Questions Like This:
|
||||
|
||||
```
|
||||
1. What is the primary goal of this feature?
|
||||
A. Improve user onboarding experience
|
||||
B. Increase user retention
|
||||
C. Reduce support burden
|
||||
D. Other: [please specify]
|
||||
|
||||
2. Who is the target user?
|
||||
A. New users only
|
||||
B. Existing users only
|
||||
C. All users
|
||||
D. Admin users only
|
||||
|
||||
3. What is the scope?
|
||||
A. Minimal viable version
|
||||
B. Full-featured implementation
|
||||
C. Just the backend/API
|
||||
D. Just the UI
|
||||
```
|
||||
|
||||
This lets users respond with "1A, 2C, 3B" for quick iteration. Remember to indent the options.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: PRD Structure
|
||||
|
||||
Generate the PRD with these sections:
|
||||
|
||||
### 1. Introduction/Overview
|
||||
Brief description of the feature and the problem it solves.
|
||||
|
||||
### 2. Goals
|
||||
Specific, measurable objectives (bullet list).
|
||||
|
||||
### 3. User Stories
|
||||
Each story needs:
|
||||
- **Title:** Short descriptive name
|
||||
- **Description:** "As a [user], I want [feature] so that [benefit]"
|
||||
- **Acceptance Criteria:** Verifiable checklist of what "done" means
|
||||
|
||||
Each story should be small enough to implement in one focused session.
|
||||
|
||||
**Format:**
|
||||
```markdown
|
||||
### US-001: [Title]
|
||||
**Description:** As a [user], I want [feature] so that [benefit].
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Specific verifiable criterion
|
||||
- [ ] Another criterion
|
||||
- [ ] Typecheck/lint passes
|
||||
- [ ] **[UI stories only]** Verify in browser using dev-browser skill
|
||||
```
|
||||
|
||||
**Important:**
|
||||
- Acceptance criteria must be verifiable, not vague. "Works correctly" is bad. "Button shows confirmation dialog before deleting" is good.
|
||||
- **For any story with UI changes:** Always include "Verify in browser using dev-browser skill" as acceptance criteria. This ensures visual verification of frontend work.
|
||||
|
||||
### 4. Functional Requirements
|
||||
Numbered list of specific functionalities:
|
||||
- "FR-1: The system must allow users to..."
|
||||
- "FR-2: When a user clicks X, the system must..."
|
||||
|
||||
Be explicit and unambiguous.
|
||||
|
||||
### 5. Non-Goals (Out of Scope)
|
||||
What this feature will NOT include. Critical for managing scope.
|
||||
|
||||
### 6. Design Considerations (Optional)
|
||||
- UI/UX requirements
|
||||
- Link to mockups if available
|
||||
- Relevant existing components to reuse
|
||||
|
||||
### 7. Technical Considerations (Optional)
|
||||
- Known constraints or dependencies
|
||||
- Integration points with existing systems
|
||||
- Performance requirements
|
||||
|
||||
### 8. Success Metrics
|
||||
How will success be measured?
|
||||
- "Reduce time to complete X by 50%"
|
||||
- "Increase conversion rate by 10%"
|
||||
|
||||
### 9. Open Questions
|
||||
Remaining questions or areas needing clarification.
|
||||
|
||||
---
|
||||
|
||||
## Writing for Junior Developers
|
||||
|
||||
The PRD reader may be a junior developer or AI agent. Therefore:
|
||||
|
||||
- Be explicit and unambiguous
|
||||
- Avoid jargon or explain it
|
||||
- Provide enough detail to understand purpose and core logic
|
||||
- Number requirements for easy reference
|
||||
- Use concrete examples where helpful
|
||||
|
||||
---
|
||||
|
||||
## Output
|
||||
|
||||
- **Format:** Markdown (`.md`)
|
||||
- **Location:** `tasks/`
|
||||
- **Filename:** `prd-[feature-name].md` (kebab-case)
|
||||
|
||||
---
|
||||
|
||||
## Example PRD
|
||||
|
||||
```markdown
|
||||
# PRD: Task Priority System
|
||||
|
||||
## Introduction
|
||||
|
||||
Add priority levels to tasks so users can focus on what matters most. Tasks can be marked as high, medium, or low priority, with visual indicators and filtering to help users manage their workload effectively.
|
||||
|
||||
## Goals
|
||||
|
||||
- Allow assigning priority (high/medium/low) to any task
|
||||
- Provide clear visual differentiation between priority levels
|
||||
- Enable filtering and sorting by priority
|
||||
- Default new tasks to medium priority
|
||||
|
||||
## User Stories
|
||||
|
||||
### US-001: Add priority field to database
|
||||
**Description:** As a developer, I need to store task priority so it persists across sessions.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Add priority column to tasks table: 'high' | 'medium' | 'low' (default 'medium')
|
||||
- [ ] Generate and run migration successfully
|
||||
- [ ] Typecheck passes
|
||||
|
||||
### US-002: Display priority indicator on task cards
|
||||
**Description:** As a user, I want to see task priority at a glance so I know what needs attention first.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Each task card shows colored priority badge (red=high, yellow=medium, gray=low)
|
||||
- [ ] Priority visible without hovering or clicking
|
||||
- [ ] Typecheck passes
|
||||
- [ ] Verify in browser using dev-browser skill
|
||||
|
||||
### US-003: Add priority selector to task edit
|
||||
**Description:** As a user, I want to change a task's priority when editing it.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Priority dropdown in task edit modal
|
||||
- [ ] Shows current priority as selected
|
||||
- [ ] Saves immediately on selection change
|
||||
- [ ] Typecheck passes
|
||||
- [ ] Verify in browser using dev-browser skill
|
||||
|
||||
### US-004: Filter tasks by priority
|
||||
**Description:** As a user, I want to filter the task list to see only high-priority items when I'm focused.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Filter dropdown with options: All | High | Medium | Low
|
||||
- [ ] Filter persists in URL params
|
||||
- [ ] Empty state message when no tasks match filter
|
||||
- [ ] Typecheck passes
|
||||
- [ ] Verify in browser using dev-browser skill
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- FR-1: Add `priority` field to tasks table ('high' | 'medium' | 'low', default 'medium')
|
||||
- FR-2: Display colored priority badge on each task card
|
||||
- FR-3: Include priority selector in task edit modal
|
||||
- FR-4: Add priority filter dropdown to task list header
|
||||
- FR-5: Sort by priority within each status column (high to medium to low)
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No priority-based notifications or reminders
|
||||
- No automatic priority assignment based on due date
|
||||
- No priority inheritance for subtasks
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
- Reuse existing badge component with color variants
|
||||
- Filter state managed via URL search params
|
||||
- Priority stored in database, not computed
|
||||
|
||||
## Success Metrics
|
||||
|
||||
- Users can change priority in under 2 clicks
|
||||
- High-priority tasks immediately visible at top of lists
|
||||
- No regression in task list performance
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should priority affect task ordering within a column?
|
||||
- Should we add keyboard shortcuts for priority changes?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before saving the PRD:
|
||||
|
||||
- [ ] Asked clarifying questions with lettered options
|
||||
- [ ] Incorporated user's answers
|
||||
- [ ] User stories are small and specific
|
||||
- [ ] Functional requirements are numbered and unambiguous
|
||||
- [ ] Non-goals section defines clear boundaries
|
||||
- [ ] Saved to `tasks/prd-[feature-name].md`
|
||||
@@ -0,0 +1,258 @@
|
||||
---
|
||||
name: ralph
|
||||
description: "Convert PRDs to prd.json format for the Ralph autonomous agent system. Use when you have an existing PRD and need to convert it to Ralph's JSON format. Triggers on: convert this prd, turn this into ralph format, create prd.json from this, ralph json."
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# Ralph PRD Converter
|
||||
|
||||
Converts existing PRDs to the prd.json format that Ralph uses for autonomous execution.
|
||||
|
||||
---
|
||||
|
||||
## The Job
|
||||
|
||||
Take a PRD (markdown file or text) and convert it to `prd.json` in your ralph directory.
|
||||
|
||||
---
|
||||
|
||||
## Output Format
|
||||
|
||||
```json
|
||||
{
|
||||
"project": "[Project Name]",
|
||||
"branchName": "ralph/[feature-name-kebab-case]",
|
||||
"description": "[Feature description from PRD title/intro]",
|
||||
"userStories": [
|
||||
{
|
||||
"id": "US-001",
|
||||
"title": "[Story title]",
|
||||
"description": "As a [user], I want [feature] so that [benefit]",
|
||||
"acceptanceCriteria": [
|
||||
"Criterion 1",
|
||||
"Criterion 2",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 1,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Story Size: The Number One Rule
|
||||
|
||||
**Each story must be completable in ONE Ralph iteration (one context window).**
|
||||
|
||||
Ralph spawns a fresh Amp instance per iteration with no memory of previous work. If a story is too big, the LLM runs out of context before finishing and produces broken code.
|
||||
|
||||
### Right-sized stories:
|
||||
- Add a database column and migration
|
||||
- Add a UI component to an existing page
|
||||
- Update a server action with new logic
|
||||
- Add a filter dropdown to a list
|
||||
|
||||
### Too big (split these):
|
||||
- "Build the entire dashboard" - Split into: schema, queries, UI components, filters
|
||||
- "Add authentication" - Split into: schema, middleware, login UI, session handling
|
||||
- "Refactor the API" - Split into one story per endpoint or pattern
|
||||
|
||||
**Rule of thumb:** If you cannot describe the change in 2-3 sentences, it is too big.
|
||||
|
||||
---
|
||||
|
||||
## Story Ordering: Dependencies First
|
||||
|
||||
Stories execute in priority order. Earlier stories must not depend on later ones.
|
||||
|
||||
**Correct order:**
|
||||
1. Schema/database changes (migrations)
|
||||
2. Server actions / backend logic
|
||||
3. UI components that use the backend
|
||||
4. Dashboard/summary views that aggregate data
|
||||
|
||||
**Wrong order:**
|
||||
1. UI component (depends on schema that does not exist yet)
|
||||
2. Schema change
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria: Must Be Verifiable
|
||||
|
||||
Each criterion must be something Ralph can CHECK, not something vague.
|
||||
|
||||
### Good criteria (verifiable):
|
||||
- "Add `status` column to tasks table with default 'pending'"
|
||||
- "Filter dropdown has options: All, Active, Completed"
|
||||
- "Clicking delete shows confirmation dialog"
|
||||
- "Typecheck passes"
|
||||
- "Tests pass"
|
||||
|
||||
### Bad criteria (vague):
|
||||
- "Works correctly"
|
||||
- "User can do X easily"
|
||||
- "Good UX"
|
||||
- "Handles edge cases"
|
||||
|
||||
### Always include as final criterion:
|
||||
```
|
||||
"Typecheck passes"
|
||||
```
|
||||
|
||||
For stories with testable logic, also include:
|
||||
```
|
||||
"Tests pass"
|
||||
```
|
||||
|
||||
### For stories that change UI, also include:
|
||||
```
|
||||
"Verify in browser using dev-browser skill"
|
||||
```
|
||||
|
||||
Frontend stories are NOT complete until visually verified. Ralph will use the dev-browser skill to navigate to the page, interact with the UI, and confirm changes work.
|
||||
|
||||
---
|
||||
|
||||
## Conversion Rules
|
||||
|
||||
1. **Each user story becomes one JSON entry**
|
||||
2. **IDs**: Sequential (US-001, US-002, etc.)
|
||||
3. **Priority**: Based on dependency order, then document order
|
||||
4. **All stories**: `passes: false` and empty `notes`
|
||||
5. **branchName**: Derive from feature name, kebab-case, prefixed with `ralph/`
|
||||
6. **Always add**: "Typecheck passes" to every story's acceptance criteria
|
||||
|
||||
---
|
||||
|
||||
## Splitting Large PRDs
|
||||
|
||||
If a PRD has big features, split them:
|
||||
|
||||
**Original:**
|
||||
> "Add user notification system"
|
||||
|
||||
**Split into:**
|
||||
1. US-001: Add notifications table to database
|
||||
2. US-002: Create notification service for sending notifications
|
||||
3. US-003: Add notification bell icon to header
|
||||
4. US-004: Create notification dropdown panel
|
||||
5. US-005: Add mark-as-read functionality
|
||||
6. US-006: Add notification preferences page
|
||||
|
||||
Each is one focused change that can be completed and verified independently.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
**Input PRD:**
|
||||
```markdown
|
||||
# Task Status Feature
|
||||
|
||||
Add ability to mark tasks with different statuses.
|
||||
|
||||
## Requirements
|
||||
- Toggle between pending/in-progress/done on task list
|
||||
- Filter list by status
|
||||
- Show status badge on each task
|
||||
- Persist status in database
|
||||
```
|
||||
|
||||
**Output prd.json:**
|
||||
```json
|
||||
{
|
||||
"project": "TaskApp",
|
||||
"branchName": "ralph/task-status",
|
||||
"description": "Task Status Feature - Track task progress with status indicators",
|
||||
"userStories": [
|
||||
{
|
||||
"id": "US-001",
|
||||
"title": "Add status field to tasks table",
|
||||
"description": "As a developer, I need to store task status in the database.",
|
||||
"acceptanceCriteria": [
|
||||
"Add status column: 'pending' | 'in_progress' | 'done' (default 'pending')",
|
||||
"Generate and run migration successfully",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 1,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-002",
|
||||
"title": "Display status badge on task cards",
|
||||
"description": "As a user, I want to see task status at a glance.",
|
||||
"acceptanceCriteria": [
|
||||
"Each task card shows colored status badge",
|
||||
"Badge colors: gray=pending, blue=in_progress, green=done",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 2,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-003",
|
||||
"title": "Add status toggle to task list rows",
|
||||
"description": "As a user, I want to change task status directly from the list.",
|
||||
"acceptanceCriteria": [
|
||||
"Each row has status dropdown or toggle",
|
||||
"Changing status saves immediately",
|
||||
"UI updates without page refresh",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 3,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-004",
|
||||
"title": "Filter tasks by status",
|
||||
"description": "As a user, I want to filter the list to see only certain statuses.",
|
||||
"acceptanceCriteria": [
|
||||
"Filter dropdown: All | Pending | In Progress | Done",
|
||||
"Filter persists in URL params",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 4,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Archiving Previous Runs
|
||||
|
||||
**Before writing a new prd.json, check if there is an existing one from a different feature:**
|
||||
|
||||
1. Read the current `prd.json` if it exists
|
||||
2. Check if `branchName` differs from the new feature's branch name
|
||||
3. If different AND `progress.txt` has content beyond the header:
|
||||
- Create archive folder: `archive/YYYY-MM-DD-feature-name/`
|
||||
- Copy current `prd.json` and `progress.txt` to archive
|
||||
- Reset `progress.txt` with fresh header
|
||||
|
||||
**The ralph.sh script handles this automatically** when you run it, but if you are manually updating prd.json between runs, archive first.
|
||||
|
||||
---
|
||||
|
||||
## Checklist Before Saving
|
||||
|
||||
Before writing prd.json, verify:
|
||||
|
||||
- [ ] **Previous run archived** (if prd.json exists with different branchName, archive it first)
|
||||
- [ ] Each story is completable in one iteration (small enough)
|
||||
- [ ] Stories are ordered by dependency (schema to backend to UI)
|
||||
- [ ] Every story has "Typecheck passes" as criterion
|
||||
- [ ] UI stories have "Verify in browser using dev-browser skill" as criterion
|
||||
- [ ] Acceptance criteria are verifiable (not vague)
|
||||
- [ ] No story depends on a later story
|
||||
@@ -0,0 +1,588 @@
|
||||
{
|
||||
"project": "GP Clinical Record — Depth Enhancement",
|
||||
"branchName": "ralph/depth-enhancement",
|
||||
"description": "Add depth, interactivity, and rich content to the GP clinical record dashboard: slide-in detail panels, sub-navigation, expanded skills/KPI data, career constellation D3 visualization, and login refresh. Full spec in Ralph/depth-design.md, requirements in Ralph/depth-requirements.md, workflow in Ralph/workflow_depth.md.",
|
||||
"userStories": [
|
||||
{
|
||||
"id": "US-001",
|
||||
"title": "Clean up unused legacy components and hooks",
|
||||
"description": "As a developer, I need to remove all dead code from the previous PMR interface so the codebase is clean before adding new features. Delete all files listed below and verify no dead imports remain.",
|
||||
"acceptanceCriteria": [
|
||||
"Delete src/components/PMRInterface.tsx",
|
||||
"Delete src/components/PatientBanner.tsx",
|
||||
"Delete src/components/ClinicalSidebar.tsx",
|
||||
"Delete src/components/Breadcrumb.tsx",
|
||||
"Delete src/components/MobileBottomNav.tsx",
|
||||
"Delete all files in src/components/views/ directory (SummaryView, ConsultationsView, MedicationsView, ProblemsView, InvestigationsView, DocumentsView, ReferralsView) and remove the views/ directory",
|
||||
"Delete src/components/Contact.tsx, Education.tsx, Experience.tsx, FloatingNav.tsx, Footer.tsx, Hero.tsx, Projects.tsx, Skills.tsx (old portfolio components)",
|
||||
"Delete src/hooks/useScrollCondensation.ts",
|
||||
"Delete src/hooks/useActiveSection.ts (will be recreated in a later story)",
|
||||
"Delete src/hooks/useScrollReveal.ts if unused",
|
||||
"Verify no remaining files import any of the deleted files (fix any dead imports)",
|
||||
"npm run build succeeds with zero errors",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 1,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-002",
|
||||
"title": "Add new TypeScript types and CSS custom properties for depth features",
|
||||
"description": "As a developer, I need new types and CSS foundations that subsequent stories will use. Add types to src/types/pmr.ts and CSS variables + keyframes to src/index.css. See Ralph/depth-design.md Section 4 for type definitions and Section 9 for CSS.",
|
||||
"acceptanceCriteria": [
|
||||
"Add SkillCategory type: 'Technical' | 'Domain' | 'Leadership' to src/types/pmr.ts",
|
||||
"Add KPIStory interface with fields: context (string), role (string), outcomes (string[]), period (string optional) to src/types/pmr.ts",
|
||||
"Add optional story?: KPIStory field to existing KPI interface in src/types/pmr.ts",
|
||||
"Add ConstellationNode interface (id, type: 'role'|'skill', label, shortLabel?, organization?, startYear?, endYear?, orgColor?, domain?) to src/types/pmr.ts",
|
||||
"Add ConstellationLink interface (source, target, strength) to src/types/pmr.ts",
|
||||
"Add DetailPanelContent discriminated union type (kpi | skill | skills-all | consultation | project | education | career-role) to src/types/pmr.ts",
|
||||
"Add EducationExtra interface (documentId, extracurriculars?, researchDescription?, programmeDetail?) to src/types/pmr.ts",
|
||||
"Add CSS custom properties to :root in src/index.css: --subnav-height: 36px, --panel-narrow: 400px, --panel-wide: 60vw, --backdrop-blur: 4px, --backdrop-bg: rgba(26,43,42,0.15)",
|
||||
"Add @keyframes panel-slide-in (translateX 100% to 0), panel-slide-out (reverse), backdrop-fade-in (opacity 0 to 1) to src/index.css",
|
||||
"Add prefers-reduced-motion overrides for all new keyframes (instant, no transform/opacity change)",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 2,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-003",
|
||||
"title": "Create DetailPanelContext, DetailPanel component, and useFocusTrap hook",
|
||||
"description": "As a developer, I need the core detail panel infrastructure: a context for managing panel state, the slide-in panel component, and a focus trap hook. Create 3 new files. The panel renders placeholder content for now (real renderers come later). See Ralph/depth-design.md Sections 2.1, 2.2 for full interface specs.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/contexts/DetailPanelContext.tsx with DetailPanelProvider that manages: content (DetailPanelContent | null), openPanel, closePanel, isOpen",
|
||||
"Width mapping is deterministic from content.type: kpi/skill/skills-all/education → 'narrow' (var(--panel-narrow)), consultation/project/career-role → 'wide' (var(--panel-wide))",
|
||||
"Title mapping derives from content data (e.g., kpi → kpi.label, skill → skill.name, consultation → consultation.role)",
|
||||
"Create src/components/DetailPanel.tsx: full-screen backdrop (var(--backdrop-bg) + backdrop-filter: blur(var(--backdrop-blur))) with panel sliding from right",
|
||||
"Panel has header with X close button (lucide X icon), colored dot matching tile, and title text",
|
||||
"Panel body is scrollable and renders placeholder text showing content type",
|
||||
"Close triggers: backdrop click, Escape key, X button",
|
||||
"ARIA: aria-modal=true, role=dialog, aria-labelledby pointing to title",
|
||||
"Mobile (<768px): both narrow and wide become 100vw",
|
||||
"prefers-reduced-motion: instant appear, no slide animation",
|
||||
"Create src/hooks/useFocusTrap.ts: useFocusTrap(containerRef, isActive) traps Tab/Shift+Tab within container when active, returns focus to previous element when deactivated",
|
||||
"DetailPanel uses useFocusTrap when open",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 3,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-004",
|
||||
"title": "Create SubNav component and useActiveSection hook",
|
||||
"description": "As a developer, I need a sticky sub-navigation bar below the TopBar for section jumping, plus a hook that tracks which section is visible. Create src/components/SubNav.tsx and src/hooks/useActiveSection.ts (the old one was deleted in cleanup). See Ralph/depth-design.md Section 2.3.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/components/SubNav.tsx with 5 sections: Overview (patient-summary), Skills (core-skills), Experience (career-activity), Projects (projects), Education (education)",
|
||||
"SubNav is sticky below TopBar (top: 48px, z-index: 99)",
|
||||
"Height 36px, background var(--surface), bottom border var(--border-light)",
|
||||
"Tabs: 13px font, weight 500, gap 24px, centered text",
|
||||
"Active tab: teal underline (2px) with 200ms slide transition, text color var(--accent)",
|
||||
"Inactive tabs: var(--text-secondary)",
|
||||
"Click scrolls smoothly to [data-tile-id=tileId] element",
|
||||
"Create src/hooks/useActiveSection.ts using IntersectionObserver to track visible tile by data-tile-id attribute",
|
||||
"Maps tile IDs to section IDs: patient-summary→overview, core-skills→skills, career-activity→experience, projects→projects, education→education",
|
||||
"SubNav accepts activeSection and onSectionClick props",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 4,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-005",
|
||||
"title": "Expand skills data from 5 to ~20 with three categories",
|
||||
"description": "As a developer, I need to expand src/data/skills.ts from 5 skills to ~21 skills across 3 categories. Source content from References/CV_v4.md Core Competencies. Each skill retains the medication metaphor (frequency, status, proficiency). See Ralph/depth-design.md Section 5.1 and Ralph/depth-requirements.md Section 4.4.",
|
||||
"acceptanceCriteria": [
|
||||
"src/data/skills.ts has ~21 SkillMedication entries",
|
||||
"Technical category (8): Data Analysis, Python, SQL, Power BI, JavaScript/TypeScript, Excel, Algorithm Design, Data Pipelines",
|
||||
"Healthcare Domain category (6): Medicines Optimisation, Population Health, NICE TA Implementation, Health Economics, Clinical Pathways, Controlled Drugs",
|
||||
"Strategic & Leadership category (7): Budget Management, Stakeholder Engagement, Pharmaceutical Negotiation, Team Development, Change Management, Financial Modelling, Executive Communication",
|
||||
"Each skill has: id (kebab-case), name, frequency (medication-style: Daily, Twice daily, Once weekly, When required, etc.), startYear, yearsOfExperience, proficiency (0-100), category, status (Active/Historical), icon (lucide icon name)",
|
||||
"Frequency and proficiency values are realistic based on CV_v4.md role descriptions",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 5,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-006",
|
||||
"title": "Add KPI story data and update 4th KPI",
|
||||
"description": "As a developer, I need to add rich story content to each KPI in src/data/kpis.ts for the detail panel, and change the 4th KPI from '12 Team Size Led' to '1.2M Population served'. Source from References/CV_v4.md. See Ralph/depth-design.md Section 5.2.",
|
||||
"acceptanceCriteria": [
|
||||
"Change 4th KPI from {id:'team', value:'12', label:'Team Size Led'} to {id:'population', value:'1.2M', label:'Population Served', sub:'Norfolk & Waveney ICS', colorVariant:'teal'}",
|
||||
"Add story field (KPIStory) to all 4 KPIs with: context, role, outcomes[], period",
|
||||
"£220M story: context about ICB prescribing budget for 1.2M population, role about forecasting models and ICB board accountability, outcomes about proactive financial planning",
|
||||
"£14.6M story: context about efficiency programme, role about data analysis identification, outcomes about over-target performance",
|
||||
"9+ Years story: context about career span Aug 2016-present, role about progression from community pharmacy to system-level leadership",
|
||||
"1.2M story: context about Norfolk & Waveney ICS population, role about population health analytics and data-driven decision making",
|
||||
"Add explanation field to 4th KPI matching the story context",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 6,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-007",
|
||||
"title": "Create education extras data file",
|
||||
"description": "As a developer, I need src/data/educationExtras.ts with expanded detail for the education detail panel. Source from References/CV_v4.md Education section. See Ralph/depth-design.md Section 5.4.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/data/educationExtras.ts exporting educationExtras array of EducationExtra objects",
|
||||
"MPharm entry (documentId matching doc-mpharm or equivalent from documents.ts): extracurriculars ['President of UEA Pharmacy Society', 'Secretary & Vice-President of UEA Ultimate Frisbee', 'Publicity Officer for UEA Alzheimer\\'s Society'], researchDescription about cocrystal formation for drug delivery",
|
||||
"Mary Seacole entry: programmeDetail about NHS leadership qualification, change management, healthcare leadership, system-level thinking",
|
||||
"Document IDs match those used in src/data/documents.ts",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 7,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-008",
|
||||
"title": "Restructure DashboardLayout with SubNav, new tile order, and DetailPanel",
|
||||
"description": "As a developer, I need to update DashboardLayout.tsx to: wrap with DetailPanelProvider, add SubNav between TopBar and content, reorder tiles per the new layout, render DetailPanel, and adjust spacing. See Ralph/depth-design.md Section 3.1.",
|
||||
"acceptanceCriteria": [
|
||||
"DashboardLayout (or App.tsx) wraps content with DetailPanelProvider from DetailPanelContext",
|
||||
"SubNav renders between TopBar and the flex container",
|
||||
"Content area marginTop accounts for both TopBar and SubNav: calc(var(--topbar-height) + var(--subnav-height))",
|
||||
"Tile order: PatientSummaryTile (full), LatestResultsTile (half) + ProjectsTile (half) side-by-side, CoreSkillsTile (full), LastConsultationTile (full), CareerActivityTile (full), EducationTile (full)",
|
||||
"DetailPanel component renders alongside CommandPalette",
|
||||
"SubNav activeSection state managed via useActiveSection hook",
|
||||
"All tiles have data-tile-id attributes (Card tileId prop)",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 8,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-009",
|
||||
"title": "Create constellation data mapping file",
|
||||
"description": "As a developer, I need src/data/constellation.ts defining the role-skill mapping for the D3 career constellation graph. Maps 6 career roles to their associated skills with connection strengths. See Ralph/depth-design.md Section 5.3 and 2.4.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/data/constellation.ts with RoleSkillMapping interface (roleId: string, skillIds: string[])",
|
||||
"Export roleSkillMappings array mapping 6 roles to skill IDs from skills.ts",
|
||||
"Roles: pre-reg-pharmacist-2015, duty-pharmacy-manager-2016, pharmacy-manager-2017, hcd-pharmacist-2022, deputy-head-2024, interim-head-2025 (IDs should match or reference consultation IDs from consultations.ts)",
|
||||
"Export constellationNodes array of ConstellationNode objects for all role nodes (with organization, startYear, endYear, orgColor) and skill nodes (with domain)",
|
||||
"Export constellationLinks array of ConstellationLink objects connecting skills to roles with strength values (0-1)",
|
||||
"Role orgColors: Paydens gets one color, Tesco another, NHS another (use distinct teal/blue/green tones)",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 9,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-010",
|
||||
"title": "Modify LatestResultsTile: remove flip, bigger numbers, panel trigger",
|
||||
"description": "As a developer, I need to redesign the KPI cards in LatestResultsTile.tsx: remove the CSS flip animation, make headline numbers larger and bolder, and make each card clickable to open the detail panel. See Ralph/depth-design.md Section 3.5.",
|
||||
"acceptanceCriteria": [
|
||||
"Remove flip card animation entirely (no more .metric-card, .metric-card-inner, .metric-card-front, .metric-card-back CSS classes from index.css if they exist)",
|
||||
"Each KPI renders as a clickable button/card with: value at 28-32px font-size, weight 700, colored by kpi.colorVariant",
|
||||
"Label at 12px, weight 500, color var(--text-primary), marginTop 4px",
|
||||
"Sub-text at 10px, font-family var(--font-geist-mono), color var(--text-tertiary), marginTop 2px",
|
||||
"Click calls openPanel({ type: 'kpi', kpi }) from DetailPanelContext",
|
||||
"Hover: border color shift + shadow deepens (transition 150ms)",
|
||||
"Keyboard: Enter/Space triggers panel open",
|
||||
"Card styling: padding 16px, background var(--surface), border 1px solid var(--border-light), border-radius var(--radius-sm)",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 10,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-011",
|
||||
"title": "Modify CoreSkillsTile: full width, categorised groups, panel triggers",
|
||||
"description": "As a developer, I need to redesign CoreSkillsTile.tsx as full-width with skills grouped by 3 categories, showing top 3-4 per category with 'view all' buttons. Individual skills and 'view all' trigger the detail panel. See Ralph/depth-design.md Section 3.4.",
|
||||
"acceptanceCriteria": [
|
||||
"Card uses full prop (spans both grid columns)",
|
||||
"Skills grouped by category: Technical, Healthcare Domain (Domain), Strategic & Leadership (Leadership)",
|
||||
"Each category has a header: thin divider line with category label (styled like sidebar section dividers: 10px, uppercase, var(--text-tertiary))",
|
||||
"Show top 3-4 skills per category on the dashboard tile (sorted by proficiency or relevance)",
|
||||
"Each skill row is clickable → openPanel({ type: 'skill', skill }) from DetailPanelContext",
|
||||
"Each category with >4 skills shows a 'View all (N)' button → openPanel({ type: 'skills-all', category })",
|
||||
"Retain medication metaphor display (frequency, status badge)",
|
||||
"Remove old single-expand accordion for skills (replaced by panel)",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 11,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-012",
|
||||
"title": "Modify ProjectsTile: half width, compact card grid, panel trigger",
|
||||
"description": "As a developer, I need to change ProjectsTile.tsx from full-width to half-width (positioned alongside LatestResultsTile by the layout reorder in US-008). Compact cards with click to open detail panel. See Ralph/depth-design.md Section 3.6.",
|
||||
"acceptanceCriteria": [
|
||||
"Remove full prop from Card (half-width, single grid column)",
|
||||
"Compact project cards: status dot + name + year (right-aligned) per row",
|
||||
"Tech stack shown as small inline tags",
|
||||
"Each project card clickable → openPanel({ type: 'project', investigation }) from DetailPanelContext",
|
||||
"Remove old in-place expansion (replaced by panel)",
|
||||
"Hover: border color shift, shadow deepens",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 12,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-013",
|
||||
"title": "Modify LastConsultationTile: add panel trigger",
|
||||
"description": "As a developer, I need to add a 'View full record' button to LastConsultationTile.tsx that opens the detail panel with full role details. See Ralph/depth-design.md Section 3.9.",
|
||||
"acceptanceCriteria": [
|
||||
"Add 'View full record' link/button at the bottom of the tile",
|
||||
"Click → openPanel({ type: 'consultation', consultation }) from DetailPanelContext, passing the first consultation entry",
|
||||
"Make the tile header area also clickable (opens same panel)",
|
||||
"Keep existing inline content (header info row, achievement bullets)",
|
||||
"Hover state on clickable areas",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 13,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-014",
|
||||
"title": "Modify CareerActivityTile: panel triggers and hover preview",
|
||||
"description": "As a developer, I need to change CareerActivityTile.tsx so timeline items click to open the detail panel instead of expanding in-place, and add hover previews. See Ralph/depth-design.md Section 3.7.",
|
||||
"acceptanceCriteria": [
|
||||
"Role timeline items click → openPanel({ type: 'career-role', consultation }) from DetailPanelContext",
|
||||
"Remove in-place accordion expansion for career items (replaced by panel)",
|
||||
"Hover preview: items lift slightly on hover with shadow deepens, show 1-2 lines of preview text",
|
||||
"Keep color-coded dots and entry type styling (teal roles, amber projects, green certs, purple education)",
|
||||
"Reserve a container/placeholder for CareerConstellation component (will be added later)",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 14,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-015",
|
||||
"title": "Modify EducationTile: richer content, panel trigger",
|
||||
"description": "As a developer, I need to enhance EducationTile.tsx with richer inline content and click-to-panel interaction. See Ralph/depth-design.md Section 3.8.",
|
||||
"acceptanceCriteria": [
|
||||
"Show richer inline content: MPharm research project score (75.1%), OSCE score (80%), A-level grades (A* Maths, B Chemistry, C Politics)",
|
||||
"Each education entry is clickable → openPanel({ type: 'education', document }) from DetailPanelContext",
|
||||
"Hover: border color shift on clickable entries",
|
||||
"Use education extras data from src/data/educationExtras.ts for inline detail where appropriate",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 15,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-016",
|
||||
"title": "Modify PatientSummaryTile: structured presentation with highlight strip",
|
||||
"description": "As a developer, I need to improve PatientSummaryTile.tsx with the full CV_v4.md profile text and a visual highlight strip. See Ralph/depth-design.md Section 3.10 and Ralph/depth-requirements.md Section 4.1.",
|
||||
"acceptanceCriteria": [
|
||||
"Verify src/data/profile.ts has the complete profile text from References/CV_v4.md (update if needed)",
|
||||
"Add a visual highlight strip showing key stats: e.g. '9+ Years Experience', '1.2M Population', '£220M Budget' as small styled badges or pills",
|
||||
"Profile text is not a wall of text — use hierarchy: bold key phrases, structured paragraphs if needed",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 16,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-017",
|
||||
"title": "Create KPIDetail renderer for detail panel",
|
||||
"description": "As a developer, I need src/components/detail/KPIDetail.tsx that renders rich KPI story content inside the detail panel. Wire it into DetailPanel so content.type === 'kpi' renders this component. See Ralph/depth-design.md Section 6.1.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/components/detail/KPIDetail.tsx accepting a KPI prop",
|
||||
"Renders: headline number (large, colored by kpi.colorVariant), context paragraph (story.context), 'Your role' paragraph (story.role), outcome bullets (story.outcomes), period badge (story.period)",
|
||||
"Graceful fallback if story is undefined (show kpi.explanation instead)",
|
||||
"Wire into DetailPanel: when content.type === 'kpi', render <KPIDetail kpi={content.kpi} />",
|
||||
"Styling matches dashboard design system (fonts, colors, spacing)",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 17,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-018",
|
||||
"title": "Create ConsultationDetail renderer for detail panel",
|
||||
"description": "As a developer, I need src/components/detail/ConsultationDetail.tsx for displaying full role details in the detail panel. Used for both 'consultation' and 'career-role' content types. See Ralph/depth-design.md Section 6.4.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/components/detail/ConsultationDetail.tsx accepting a Consultation prop",
|
||||
"Renders: role title + organization + dates, history paragraph (consultation.history), achievement bullets (consultation.examination), plan/outcomes (consultation.plan), coded entries as badges (consultation.codedEntries)",
|
||||
"Wire into DetailPanel: content.type === 'consultation' or 'career-role' renders this component",
|
||||
"Styled consistently with dashboard design system",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 18,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-019",
|
||||
"title": "Create ProjectDetail renderer for detail panel",
|
||||
"description": "As a developer, I need src/components/detail/ProjectDetail.tsx for displaying full project information in the wide detail panel. See Ralph/depth-design.md Section 6.5.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/components/detail/ProjectDetail.tsx accepting an Investigation prop",
|
||||
"Renders: project name + year + status badge, methodology description, tech stack as tags, results bullets, external link button (if investigation.externalUrl exists, opens in new tab)",
|
||||
"Wire into DetailPanel: content.type === 'project' renders this component",
|
||||
"External link uses rel='noopener noreferrer'",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 19,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-020",
|
||||
"title": "Create SkillDetail renderer for detail panel",
|
||||
"description": "As a developer, I need src/components/detail/SkillDetail.tsx for displaying individual skill detail in the narrow detail panel. See Ralph/depth-design.md Section 6.2.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/components/detail/SkillDetail.tsx accepting a SkillMedication prop",
|
||||
"Renders: skill name + frequency + status badge, visual proficiency bar (0-100%), years of experience, category label",
|
||||
"If constellation data is available, show 'Used in' section listing roles that used this skill (import from src/data/constellation.ts)",
|
||||
"Wire into DetailPanel: content.type === 'skill' renders this component",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 20,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-021",
|
||||
"title": "Create SkillsAllDetail renderer for detail panel",
|
||||
"description": "As a developer, I need src/components/detail/SkillsAllDetail.tsx showing the full categorised list of all skills. Clicking an individual skill switches the panel to SkillDetail. See Ralph/depth-design.md Section 6.3.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/components/detail/SkillsAllDetail.tsx",
|
||||
"Shows full list grouped by Technical / Healthcare Domain / Strategic & Leadership",
|
||||
"Category headers styled consistently with CoreSkillsTile category headers",
|
||||
"Each skill row is clickable → calls openPanel({ type: 'skill', skill }) to switch panel content",
|
||||
"If opened with a category filter (content.category), scroll to or highlight that category",
|
||||
"Wire into DetailPanel: content.type === 'skills-all' renders this component",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 21,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-022",
|
||||
"title": "Create EducationDetail renderer for detail panel",
|
||||
"description": "As a developer, I need src/components/detail/EducationDetail.tsx for displaying full education details including extras. See Ralph/depth-design.md Section 6.6.",
|
||||
"acceptanceCriteria": [
|
||||
"Create src/components/detail/EducationDetail.tsx accepting a Document prop",
|
||||
"Renders: title + institution + dates + classification",
|
||||
"Imports educationExtras from src/data/educationExtras.ts and finds matching extra by document ID",
|
||||
"If MPharm: shows research project description, extracurricular activities list",
|
||||
"If Mary Seacole: shows programme detail",
|
||||
"Shows notes from document data if present",
|
||||
"Wire into DetailPanel: content.type === 'education' renders this component",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 22,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-023",
|
||||
"title": "Install D3 and scaffold CareerConstellation component",
|
||||
"description": "As a developer, I need to install d3 as a dependency and create a scaffolded CareerConstellation component with an SVG container. See Ralph/depth-design.md Section 2.4.",
|
||||
"acceptanceCriteria": [
|
||||
"Run npm install d3 @types/d3",
|
||||
"Create src/components/CareerConstellation.tsx with props: onRoleClick(id: string), onSkillClick(id: string)",
|
||||
"Component renders a responsive SVG container using useRef<SVGSVGElement>",
|
||||
"Container: full width, height 400px desktop / 300px tablet / 250px mobile (use CSS or media queries)",
|
||||
"SVG has viewBox for responsive scaling",
|
||||
"Import constellation data from src/data/constellation.ts",
|
||||
"Subtle radial gradient background from var(--bg-dashboard) center to var(--surface) edge",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 23,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-024",
|
||||
"title": "Build D3 force-directed graph rendering in CareerConstellation",
|
||||
"description": "As a developer, I need the D3 force simulation to render role and skill nodes with links in the CareerConstellation component. D3 operates imperatively via useEffect on the SVG ref. See Ralph/depth-design.md Section 2.4 for exact force configuration.",
|
||||
"acceptanceCriteria": [
|
||||
"D3 force simulation with: forceManyBody(-200), forceLink(distance 80, strength from data), forceX chronological (roles positioned left-to-right by startYear), forceY centered, forceCollide(30)",
|
||||
"Role nodes: 24px radius circles, filled with orgColor, white text label",
|
||||
"Skill nodes: 10px radius, color-coded by domain: clinical=var(--success), technical=var(--accent), leadership=var(--amber)",
|
||||
"Links: thin lines (1px), var(--border) color, opacity 0.3",
|
||||
"D3 integration: useEffect on SVG ref, no React state for node positions",
|
||||
"Simulation runs and nodes settle into stable positions",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 24,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-025",
|
||||
"title": "Add accessibility to CareerConstellation",
|
||||
"description": "As a developer, I need the CareerConstellation to be accessible: keyboard navigable, screen-reader friendly, and respecting reduced motion. See Ralph/depth-design.md Section 2.4 accessibility notes.",
|
||||
"acceptanceCriteria": [
|
||||
"SVG has role=img and aria-label describing the graph ('Career constellation showing roles and skills across career timeline')",
|
||||
"Screen-reader-only text description of graph structure (hidden visually, available to assistive tech)",
|
||||
"Keyboard navigation: Tab through role nodes, Enter/Space opens detail panel for focused node",
|
||||
"Focus indicators visible on keyboard-focused nodes",
|
||||
"prefers-reduced-motion: disable force simulation animation, render nodes at calculated static positions immediately",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 25,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-026",
|
||||
"title": "Add hover and click interactions to CareerConstellation",
|
||||
"description": "As a developer, I need hover highlighting and click-to-panel interactions on the CareerConstellation. This connects the graph to the detail panel system. See Ralph/depth-design.md Section 2.4.",
|
||||
"acceptanceCriteria": [
|
||||
"Hover role node: connected skill nodes scale up, links brighten to var(--accent), non-connected nodes fade to 0.15 opacity",
|
||||
"Hover skill node: all connected role nodes highlight, link paths illuminate",
|
||||
"Click role node: calls onRoleClick(id) prop",
|
||||
"Click skill node: calls onSkillClick(id) prop",
|
||||
"Integrate into CareerActivityTile: wire onRoleClick to open ConsultationDetail panel, onSkillClick to open SkillDetail panel",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 26,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-027",
|
||||
"title": "Restyle LoginScreen with teal accents",
|
||||
"description": "As a developer, I need to visually refresh the LoginScreen with teal accents replacing the current blue. See Ralph/depth-design.md Section 3.3 and Ralph/depth-requirements.md Section 5.",
|
||||
"acceptanceCriteria": [
|
||||
"Replace #005EB8 with #0D6E6E throughout LoginScreen (shield icon bg, active field border, cursor, button)",
|
||||
"Replace #004D9F with #0A8080 (button hover state)",
|
||||
"Replace #004494 with #085858 (button pressed state)",
|
||||
"Background color: keep #1E293B or change to #1A2B2A",
|
||||
"Login card feels cohesive with the dashboard teal palette",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 27,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-028",
|
||||
"title": "Change login username to a.recruiter and add connection status indicator",
|
||||
"description": "As a developer, I need to change the typed username from a.charlwood to a.recruiter and add a connection status indicator below the login button. See Ralph/depth-design.md Section 3.3.",
|
||||
"acceptanceCriteria": [
|
||||
"Username typed in login animation is 'a.recruiter' (not 'A.CHARLWOOD' or similar)",
|
||||
"Connection status indicator appears below the login button: 6px dot + text",
|
||||
"Initial state: red/alert dot + 'Awaiting secure connection...' (var(--alert) color)",
|
||||
"After ~2000ms: dot transitions to green + 'Secure connection established' (var(--success) color, 300ms transition)",
|
||||
"Text: 10px, font-family var(--font-geist-mono), color var(--text-tertiary)",
|
||||
"Login button disabled until BOTH typing is complete AND connectionState === 'connected'",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 28,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-029",
|
||||
"title": "Add post-login loading state and update TopBar session name",
|
||||
"description": "As a developer, I need a brief loading state after clicking the login button before the dashboard appears, and the TopBar should show A.RECRUITER as the session user. See Ralph/depth-design.md Sections 3.3 and 3.2.",
|
||||
"acceptanceCriteria": [
|
||||
"On login button click: isLoading=true, card content replaced with spinner + 'Loading clinical records...' text",
|
||||
"Loading state lasts ~600ms, then calls onComplete() to transition to dashboard",
|
||||
"Spinner is a CSS-animated spinner (not a GIF), styled with var(--accent) or similar",
|
||||
"Loading text: 12px, color var(--text-secondary)",
|
||||
"In TopBar.tsx: change session display name from 'Dr. A.CHARLWOOD' (or current value) to 'A.RECRUITER'",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 29,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-030",
|
||||
"title": "Update CommandPalette for expanded content and panel actions",
|
||||
"description": "As a developer, I need to update the CommandPalette search index and actions to work with the expanded skills data (~20 skills) and add actions that open the detail panel directly. See Ralph/depth-design.md Section 10, Phase 6.",
|
||||
"acceptanceCriteria": [
|
||||
"Search index in src/lib/search.ts includes all ~21 skills (not just the original 5)",
|
||||
"Selecting a skill result opens the detail panel for that skill (openPanel call or dispatch event)",
|
||||
"Selecting a KPI result opens the KPI detail panel",
|
||||
"Selecting a project result opens the project detail panel",
|
||||
"Ensure DashboardLayout handlePaletteAction supports a new 'panel' action type or adapts existing types to trigger detail panel",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 30,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-031",
|
||||
"title": "Responsive testing and fixes for all new components",
|
||||
"description": "As a developer, I need to verify and fix responsive behavior for the detail panel, sub-nav, constellation, and restructured layout at all breakpoints.",
|
||||
"acceptanceCriteria": [
|
||||
"DetailPanel: both narrow and wide render as 100vw on mobile (<768px)",
|
||||
"SubNav: works on tablet/mobile (horizontal scroll if needed, no overflow)",
|
||||
"CareerConstellation: renders at 300px height on tablet, 250px on mobile",
|
||||
"Projects + KPIs: stack vertically on mobile when grid falls to single column",
|
||||
"CoreSkillsTile: full-width layout works on all breakpoints",
|
||||
"All interactive elements have touch targets >= 44px on mobile",
|
||||
"No horizontal overflow at 375px viewport width",
|
||||
"Typecheck passes",
|
||||
"Verify in browser using dev-browser skill"
|
||||
],
|
||||
"priority": 31,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"id": "US-032",
|
||||
"title": "Reduced motion audit, final cleanup, and visual review",
|
||||
"description": "As a developer, I need to verify all new animations respect prefers-reduced-motion, remove any dead code introduced during development, and do a final build verification.",
|
||||
"acceptanceCriteria": [
|
||||
"DetailPanel slide animation: instant appear with prefers-reduced-motion",
|
||||
"Backdrop fade: instant with prefers-reduced-motion",
|
||||
"SubNav underline transition: instant with prefers-reduced-motion",
|
||||
"CareerConstellation: static layout (no force simulation animation) with prefers-reduced-motion",
|
||||
"Connection status dot transition: instant with prefers-reduced-motion",
|
||||
"Post-login spinner: static indicator with prefers-reduced-motion",
|
||||
"No dead imports across all files",
|
||||
"Remove any unused flip card CSS (.metric-card-inner etc.) if still present in index.css",
|
||||
"npm run build succeeds cleanly",
|
||||
"npm run typecheck passes with zero errors",
|
||||
"npm run lint passes (pre-existing AccessibilityContext warning OK)",
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 32,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Ralph Progress — GP Clinical Record Depth Enhancement
|
||||
|
||||
Branch: ralph/depth-enhancement
|
||||
Stories: 32 (US-001 through US-032)
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
No iterations completed yet.
|
||||
@@ -146,51 +146,51 @@ Replace the "CareerRecord PMR" sidebar-nav + view-switching interface with a til
|
||||
|
||||
#### Task 16: Tile expansion system
|
||||
> Detail: `Ralph/refs/ref-07-interactions.md` (Tile Expansion section)
|
||||
- [ ] CareerActivity items expand to show full role detail
|
||||
- [ ] Projects items expand to show methodology, tech stack, results
|
||||
- [ ] CoreSkills items expand to show prescribing history
|
||||
- [ ] Height-only animation (200ms, no opacity fade)
|
||||
- [ ] Single-expand accordion
|
||||
- [ ] Keyboard: Enter/Space to expand, Escape to collapse
|
||||
- [ ] Run quality checks
|
||||
- [x] CareerActivity items expand to show full role detail
|
||||
- [x] Projects items expand to show methodology, tech stack, results
|
||||
- [x] CoreSkills items expand to show prescribing history
|
||||
- [x] Height-only animation (200ms, no opacity fade)
|
||||
- [x] Single-expand accordion
|
||||
- [x] Keyboard: Enter/Space to expand, Escape to collapse
|
||||
- [x] Run quality checks
|
||||
|
||||
#### Task 17: KPI flip card interaction
|
||||
> Detail: `Ralph/refs/ref-07-interactions.md` (KPI Flip section)
|
||||
- [ ] LatestResults metrics flip on click
|
||||
- [ ] Front: value + label. Back: explanation text
|
||||
- [ ] CSS perspective flip (400ms) or instant swap with reduced motion
|
||||
- [ ] One card flipped at a time
|
||||
- [ ] Run quality checks
|
||||
- [x] LatestResults metrics flip on click
|
||||
- [x] Front: value + label. Back: explanation text
|
||||
- [x] CSS perspective flip (400ms) or instant swap with reduced motion
|
||||
- [x] One card flipped at a time
|
||||
- [x] Run quality checks
|
||||
|
||||
#### Task 18: Build Command Palette
|
||||
> Detail: `Ralph/refs/ref-07-interactions.md` (Command Palette section)
|
||||
- [ ] Create `src/components/CommandPalette.tsx`
|
||||
- [ ] Ctrl+K trigger + search bar click trigger
|
||||
- [ ] Overlay with backdrop blur, ESC to close
|
||||
- [ ] Fuzzy search via fuse.js (adapt `src/lib/search.ts`)
|
||||
- [ ] Grouped results by section + Quick Actions
|
||||
- [ ] Keyboard navigation (arrows, Enter, Escape)
|
||||
- [ ] Run quality checks
|
||||
- [x] Create `src/components/CommandPalette.tsx`
|
||||
- [x] Ctrl+K trigger + search bar click trigger
|
||||
- [x] Overlay with backdrop blur, ESC to close
|
||||
- [x] Fuzzy search via fuse.js (adapt `src/lib/search.ts`)
|
||||
- [x] Grouped results by section + Quick Actions
|
||||
- [x] Keyboard navigation (arrows, Enter, Escape)
|
||||
- [x] Run quality checks
|
||||
|
||||
### Phase 4: Polish
|
||||
|
||||
#### Task 19: Responsive design
|
||||
> Detail: `Ralph/refs/ref-08-polish.md` (Responsive section)
|
||||
- [ ] Desktop (>1024px): full sidebar + 2-column grid
|
||||
- [ ] Tablet (768–1024px): collapsed/hidden sidebar + adapted grid
|
||||
- [ ] Mobile (<768px): no sidebar, single-column tiles, simplified topbar
|
||||
- [ ] Touch-friendly targets (48px+)
|
||||
- [ ] Run quality checks
|
||||
- [x] Desktop (>1024px): full sidebar + 2-column grid
|
||||
- [x] Tablet (768–1024px): collapsed/hidden sidebar + adapted grid
|
||||
- [x] Mobile (<768px): no sidebar, single-column tiles, simplified topbar
|
||||
- [x] Touch-friendly targets (48px+)
|
||||
- [x] Run quality checks
|
||||
|
||||
#### Task 20: Accessibility audit
|
||||
> Detail: `Ralph/refs/ref-08-polish.md` (Accessibility section)
|
||||
- [ ] Semantic HTML (header, nav, main, article, section)
|
||||
- [ ] Keyboard navigation (Tab, Enter/Space, Escape, Ctrl+K, arrows)
|
||||
- [ ] ARIA (expanded, controls, labels, live regions, dialog)
|
||||
- [ ] Focus management (trap in palette, visible rings, return focus)
|
||||
- [ ] `prefers-reduced-motion` on all animations
|
||||
- [ ] Color contrast verification
|
||||
- [ ] Run quality checks
|
||||
- [x] Semantic HTML (header, nav, main, article, section)
|
||||
- [x] Keyboard navigation (Tab, Enter/Space, Escape, Ctrl+K, arrows)
|
||||
- [x] ARIA (expanded, controls, labels, live regions, dialog)
|
||||
- [x] Focus management (trap in palette, visible rings, return focus)
|
||||
- [x] `prefers-reduced-motion` on all animations
|
||||
- [x] Color contrast verification
|
||||
- [x] Run quality checks
|
||||
|
||||
#### Task 21: Clean up and final polish
|
||||
> Detail: `Ralph/refs/ref-08-polish.md` (Cleanup section)
|
||||
|
||||
@@ -0,0 +1,982 @@
|
||||
# Component Architecture Design: Adding Depth
|
||||
|
||||
> Design document — Feb 2026
|
||||
> Follows requirements in `Ralph/depth-requirements.md`
|
||||
> Based on audit of current codebase architecture
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
### Current Component Tree
|
||||
```
|
||||
App.tsx (Phase: boot → ecg → login → pmr)
|
||||
└── AccessibilityProvider
|
||||
├── BootSequence (locked)
|
||||
├── ECGAnimation (locked)
|
||||
├── LoginScreen
|
||||
└── DashboardLayout
|
||||
├── TopBar
|
||||
├── Sidebar
|
||||
├── Main Content Grid
|
||||
│ ├── PatientSummaryTile
|
||||
│ ├── LatestResultsTile + CoreSkillsTile
|
||||
│ ├── LastConsultationTile
|
||||
│ ├── CareerActivityTile
|
||||
│ ├── EducationTile
|
||||
│ └── ProjectsTile
|
||||
└── CommandPalette
|
||||
```
|
||||
|
||||
### Proposed Component Tree
|
||||
```
|
||||
App.tsx (Phase: boot → ecg → login → pmr)
|
||||
└── AccessibilityProvider
|
||||
├── BootSequence (locked)
|
||||
├── ECGAnimation (locked)
|
||||
├── LoginScreen ← MODIFIED (visual refresh, a.recruiter, connection status)
|
||||
└── DetailPanelProvider ← NEW (context for panel state)
|
||||
└── DashboardLayout ← MODIFIED (sub-nav, new tile order)
|
||||
├── TopBar ← MODIFIED (session shows a.recruiter)
|
||||
├── SubNav ← NEW (section jump bar)
|
||||
├── Sidebar (unchanged)
|
||||
├── Main Content Grid ← REORDERED
|
||||
│ ├── PatientSummaryTile ← MODIFIED (CV_v4.md profile)
|
||||
│ ├── LatestResultsTile ← MODIFIED (bigger numbers, panel trigger)
|
||||
│ ├── ProjectsTile ← MOVED UP (card grid with thumbnails)
|
||||
│ ├── CoreSkillsTile ← MOVED, FULL WIDTH (categorised groups)
|
||||
│ ├── LastConsultationTile ← MODIFIED (panel trigger)
|
||||
│ ├── CareerActivityTile ← MODIFIED (constellation embedded)
|
||||
│ │ └── CareerConstellation ← NEW (D3.js force graph)
|
||||
│ └── EducationTile ← MODIFIED (richer content, panel trigger)
|
||||
├── DetailPanel ← NEW (slide-in from right)
|
||||
└── CommandPalette ← UPDATED (new panel actions)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. New Components
|
||||
|
||||
### 2.1 DetailPanel (`src/components/DetailPanel.tsx`)
|
||||
|
||||
The primary mechanism for depth. A slide-in panel from the right edge.
|
||||
|
||||
**Props Interface:**
|
||||
```typescript
|
||||
interface DetailPanelProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
width: 'narrow' | 'wide' // narrow: 400px, wide: 60vw
|
||||
title: string // Header text
|
||||
dotColor: CardHeaderProps['dotColor'] // Matches tile dot color
|
||||
children: React.ReactNode // Content rendered inside
|
||||
}
|
||||
```
|
||||
|
||||
**Behaviour:**
|
||||
- Renders a full-screen backdrop (`rgba(26,43,42,0.15)` + `backdrop-filter: blur(4px)`) and a panel div
|
||||
- Panel slides in from `translateX(100%)` → `translateX(0)` over 250ms ease-out
|
||||
- Backdrop fades in over 150ms
|
||||
- Close: click backdrop, press Escape, or click X button
|
||||
- Focus trap: first focusable element receives focus on open; Tab cycles within panel; focus returns to trigger element on close
|
||||
- `aria-modal="true"`, `role="dialog"`, `aria-labelledby` pointing to title
|
||||
- `prefers-reduced-motion`: skip slide animation, instant appear
|
||||
|
||||
**Layout:**
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Blurred backdrop (click to close) │
|
||||
│ ┌────────────────────────────┐│
|
||||
│ │ ── X close button ──────── ││
|
||||
│ │ ││
|
||||
│ │ [dot] SECTION TITLE ││
|
||||
│ │ ││
|
||||
│ │ {children} ││
|
||||
│ │ ││
|
||||
│ │ (scrollable) ││
|
||||
│ │ ││
|
||||
│ └────────────────────────────┘│
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**CSS Custom Properties to add (index.css):**
|
||||
```css
|
||||
--panel-narrow: 400px;
|
||||
--panel-wide: 60vw;
|
||||
--backdrop-blur: 4px;
|
||||
--backdrop-bg: rgba(26,43,42,0.15);
|
||||
```
|
||||
|
||||
**Responsive:**
|
||||
- On mobile (< 768px), both `narrow` and `wide` become full-width (100vw)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 DetailPanelContext (`src/contexts/DetailPanelContext.tsx`)
|
||||
|
||||
Manages what content is displayed in the detail panel. Any tile can trigger it.
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
// Union type for all possible detail panel content
|
||||
type DetailPanelContent =
|
||||
| { type: 'kpi'; kpi: KPI }
|
||||
| { type: 'skill'; skill: SkillMedication }
|
||||
| { type: 'skills-all'; category?: SkillCategory }
|
||||
| { type: 'consultation'; consultation: Consultation }
|
||||
| { type: 'project'; investigation: Investigation }
|
||||
| { type: 'education'; document: Document }
|
||||
| { type: 'career-role'; consultation: Consultation } // from constellation click
|
||||
|
||||
interface DetailPanelContextValue {
|
||||
content: DetailPanelContent | null
|
||||
openPanel: (content: DetailPanelContent) => void
|
||||
closePanel: () => void
|
||||
isOpen: boolean
|
||||
}
|
||||
```
|
||||
|
||||
**Width mapping** (deterministic from content type):
|
||||
```typescript
|
||||
const widthMap: Record<DetailPanelContent['type'], 'narrow' | 'wide'> = {
|
||||
'kpi': 'narrow',
|
||||
'skill': 'narrow',
|
||||
'skills-all': 'narrow',
|
||||
'consultation': 'wide',
|
||||
'project': 'wide',
|
||||
'education': 'narrow',
|
||||
'career-role': 'wide',
|
||||
}
|
||||
```
|
||||
|
||||
**Title mapping** (from content type + data):
|
||||
```typescript
|
||||
function getPanelTitle(content: DetailPanelContent): string {
|
||||
switch (content.type) {
|
||||
case 'kpi': return content.kpi.label
|
||||
case 'skill': return content.skill.name
|
||||
case 'skills-all': return 'All Medications'
|
||||
case 'consultation': return content.consultation.role
|
||||
case 'project': return content.investigation.name
|
||||
case 'education': return content.document.title
|
||||
case 'career-role': return content.consultation.role
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Integration:** Wraps `DashboardLayout` in `App.tsx`. The `DetailPanel` component reads from this context and renders the appropriate content.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 SubNav (`src/components/SubNav.tsx`)
|
||||
|
||||
Section jump bar positioned between TopBar and content.
|
||||
|
||||
**Props Interface:**
|
||||
```typescript
|
||||
interface SubNavProps {
|
||||
activeSection: string
|
||||
onSectionClick: (sectionId: string) => void
|
||||
}
|
||||
|
||||
interface NavSection {
|
||||
id: string
|
||||
label: string
|
||||
tileId: string // data-tile-id to scroll to
|
||||
}
|
||||
```
|
||||
|
||||
**Sections:**
|
||||
```typescript
|
||||
const sections: NavSection[] = [
|
||||
{ id: 'overview', label: 'Overview', tileId: 'patient-summary' },
|
||||
{ id: 'skills', label: 'Skills', tileId: 'core-skills' },
|
||||
{ id: 'experience', label: 'Experience', tileId: 'career-activity' },
|
||||
{ id: 'projects', label: 'Projects', tileId: 'projects' },
|
||||
{ id: 'education', label: 'Education', tileId: 'education' },
|
||||
]
|
||||
```
|
||||
|
||||
**Behaviour:**
|
||||
- Fixed/sticky position below TopBar (top: 48px)
|
||||
- Click → smooth-scroll to `[data-tile-id="${tileId}"]`
|
||||
- Active section determined by `useActiveSection` hook (IntersectionObserver on tile elements)
|
||||
- Active tab: teal underline (2px), text colour shifts to `var(--accent)`
|
||||
- Inactive tabs: `var(--text-secondary)`
|
||||
|
||||
**Style:**
|
||||
- Height: 36px
|
||||
- Background: `var(--surface)` with bottom border `var(--border-light)`
|
||||
- Tabs: 13px, font-weight 500, horizontal gap 24px, centred text
|
||||
- Teal underline on active (2px, slides with 200ms transition)
|
||||
- z-index: 99 (below TopBar at 100, above content)
|
||||
|
||||
**Existing hook to extend:** `src/hooks/useActiveSection.ts` — currently exists but may need updating to observe the correct tile IDs.
|
||||
|
||||
**CSS to add (index.css):**
|
||||
```css
|
||||
--subnav-height: 36px;
|
||||
```
|
||||
|
||||
**Layout impact:** `marginTop` on the flex container below TopBar changes from `var(--topbar-height)` to `calc(var(--topbar-height) + var(--subnav-height))`.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 CareerConstellation (`src/components/CareerConstellation.tsx`)
|
||||
|
||||
D3.js force-directed network graph embedded in the CareerActivityTile.
|
||||
|
||||
**Props Interface:**
|
||||
```typescript
|
||||
interface CareerConstellationProps {
|
||||
onRoleClick: (consultationId: string) => void
|
||||
onSkillClick: (skillId: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
**Data Model:**
|
||||
```typescript
|
||||
interface ConstellationNode {
|
||||
id: string
|
||||
type: 'role' | 'skill'
|
||||
label: string
|
||||
// Role-specific:
|
||||
organization?: string
|
||||
startYear?: number
|
||||
endYear?: number
|
||||
orgColor?: string
|
||||
// Skill-specific:
|
||||
domain?: 'clinical' | 'technical' | 'leadership'
|
||||
}
|
||||
|
||||
interface ConstellationLink {
|
||||
source: string // node id
|
||||
target: string // node id
|
||||
strength: number // 0-1, how strongly connected
|
||||
}
|
||||
```
|
||||
|
||||
**Node Data (from consultations + skills + new mapping data):**
|
||||
|
||||
Role nodes (6, positioned chronologically):
|
||||
1. Pre-Reg Pharmacist, Paydens (2015-2016)
|
||||
2. Duty Pharmacy Manager, Tesco (2016-2017)
|
||||
3. Pharmacy Manager, Tesco (2017-2022)
|
||||
4. High-Cost Drugs Pharmacist, NHS (2022-2024)
|
||||
5. Deputy Head, NHS (2024-present)
|
||||
6. Interim Head, NHS (2025)
|
||||
|
||||
Skill nodes (drawn from skills.ts + new expanded skills data):
|
||||
- Technical: Python, SQL, Power BI, JavaScript/TypeScript, Data Analysis, Algorithm Design, Excel
|
||||
- Clinical: Medicines Optimisation, Clinical Pathways, Controlled Drugs, NICE TAs, Patient Safety
|
||||
- Leadership: Budget Management, Team Development, Stakeholder Engagement, Change Management
|
||||
|
||||
Links connect skills to the roles where they were used/developed.
|
||||
|
||||
**D3 Integration Pattern:**
|
||||
- Use a `useRef<SVGSVGElement>` to get the SVG container
|
||||
- D3 operates on the SVG imperatively via `useEffect`
|
||||
- React handles the wrapper container, D3 handles the graph rendering
|
||||
- No React state for individual node positions (performance)
|
||||
- Tooltip/hover state managed via D3 event handlers dispatching to React state for the detail panel
|
||||
|
||||
**Force Simulation Configuration:**
|
||||
```typescript
|
||||
d3.forceSimulation(nodes)
|
||||
.force('charge', d3.forceManyBody().strength(-200))
|
||||
.force('link', d3.forceLink(links).distance(80).strength(d => d.strength))
|
||||
.force('x', d3.forceX(d => xScale(d.startYear)).strength(0.3)) // chronological
|
||||
.force('y', d3.forceY(height / 2).strength(0.1))
|
||||
.force('collision', d3.forceCollide(30))
|
||||
```
|
||||
|
||||
The `forceX` with a time scale ensures roles flow left-to-right chronologically. Skill nodes cluster around their associated roles.
|
||||
|
||||
**Visual Design:**
|
||||
- Role nodes: 24px radius circles, filled with `orgColor`, white text label
|
||||
- Skill nodes: 10px radius, colour-coded by domain (clinical=`var(--success)`, technical=`var(--accent)`, leadership=`var(--amber)`)
|
||||
- Links: thin lines (1px), `var(--border)` colour, opacity 0.3
|
||||
- Hover role: connected skill nodes scale up, links brighten to `var(--accent)`, non-connected nodes fade to 0.15 opacity
|
||||
- Hover skill: all connected role nodes highlight, link paths illuminate
|
||||
- Click: dispatches to `onRoleClick` / `onSkillClick` → opens detail panel
|
||||
|
||||
**Container:**
|
||||
- Full width of the CareerActivityTile
|
||||
- Height: 400px (desktop), 300px (tablet), 250px (mobile)
|
||||
- Background: subtle radial gradient from `var(--bg-dashboard)` centre to `var(--surface)` edge
|
||||
- SVG fills the container with viewBox for responsiveness
|
||||
|
||||
**New dependency:** `d3` (specifically `d3-force`, `d3-selection`, `d3-scale`, `d3-transition`)
|
||||
|
||||
**New data file:** `src/data/constellation.ts` — defines the role-skill mapping:
|
||||
```typescript
|
||||
export interface RoleSkillMapping {
|
||||
roleId: string // matches consultation.id
|
||||
skillIds: string[] // matches skill IDs
|
||||
}
|
||||
|
||||
export const roleSkillMappings: RoleSkillMapping[] = [
|
||||
{
|
||||
roleId: 'duty-pharmacist-2016',
|
||||
skillIds: ['patient-care', 'medicines-optimisation', 'team-development'],
|
||||
},
|
||||
{
|
||||
roleId: 'pharmacy-manager-2017',
|
||||
skillIds: ['patient-care', 'medicines-optimisation', 'team-development', 'data-analysis', 'excel', 'change-management', 'budget-management'],
|
||||
},
|
||||
// ... etc for all roles
|
||||
]
|
||||
```
|
||||
|
||||
**Accessibility:**
|
||||
- `role="img"` on SVG with `aria-label="Career constellation showing roles and skills across career timeline"`
|
||||
- Screen-reader-only text description of the graph structure
|
||||
- Keyboard navigation: Tab through role nodes, Enter to open detail panel
|
||||
- `prefers-reduced-motion`: disable force simulation animation, render static layout
|
||||
|
||||
---
|
||||
|
||||
## 3. Modified Components
|
||||
|
||||
### 3.1 DashboardLayout — Modifications
|
||||
|
||||
**Changes:**
|
||||
1. **Import and render SubNav** between TopBar and content flex container
|
||||
2. **Reorder tiles:** PatientSummary → LatestResults + Projects → CoreSkills → LastConsultation → CareerActivity → Education
|
||||
3. **Wrap in DetailPanelProvider** (or this wraps from App.tsx)
|
||||
4. **Render DetailPanel** alongside CommandPalette
|
||||
5. **Adjust marginTop** to account for SubNav height
|
||||
|
||||
**Updated grid in DashboardLayout:**
|
||||
```tsx
|
||||
<div className="dashboard-grid">
|
||||
<PatientSummaryTile /> {/* full width */}
|
||||
<LatestResultsTile /> {/* half width (left) */}
|
||||
<ProjectsTile /> {/* half width (right) — MOVED UP */}
|
||||
<CoreSkillsTile /> {/* full width — MOVED, was half */}
|
||||
<LastConsultationTile /> {/* full width */}
|
||||
<CareerActivityTile /> {/* full width — now includes constellation */}
|
||||
<EducationTile /> {/* full width */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**SubNav integration:**
|
||||
```tsx
|
||||
<motion.div initial="hidden" animate="visible" variants={topbarVariants}>
|
||||
<TopBar onSearchClick={handleSearchClick} />
|
||||
</motion.div>
|
||||
<SubNav activeSection={activeSection} onSectionClick={handleSectionClick} />
|
||||
{/* ... rest of layout with adjusted marginTop */}
|
||||
```
|
||||
|
||||
**New CSS variable reference:**
|
||||
- Content area `marginTop`: `calc(var(--topbar-height) + var(--subnav-height))`
|
||||
- Content area `height`: `calc(100vh - var(--topbar-height) - var(--subnav-height))`
|
||||
|
||||
---
|
||||
|
||||
### 3.2 TopBar — Modifications
|
||||
|
||||
**Changes:**
|
||||
1. Session user name: `Dr. A.CHARLWOOD` → `A.RECRUITER`
|
||||
2. No structural changes otherwise
|
||||
|
||||
**Specific change:**
|
||||
```tsx
|
||||
// Line ~172: Change display name
|
||||
<span ...>A.RECRUITER</span>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 LoginScreen — Modifications
|
||||
|
||||
**Changes:**
|
||||
1. **Username:** `A.CHARLWOOD` → `A.RECRUITER`
|
||||
2. **Visual refresh:** Teal accents replacing NHS blue (`#005EB8` → `var(--accent)` / `#0D6E6E`)
|
||||
3. **Connection status indicator:** New state machine below the login button
|
||||
4. **Post-login loading state:** Brief "System loading..." before dashboard materialises
|
||||
5. **Background colour:** `#1E293B` → consider matching `var(--bg-dashboard)` or a darker variant
|
||||
|
||||
**New state additions:**
|
||||
```typescript
|
||||
type ConnectionState = 'connecting' | 'connected'
|
||||
|
||||
const [connectionState, setConnectionState] = useState<ConnectionState>('connecting')
|
||||
const [isLoading, setIsLoading] = useState(false) // post-click loading state
|
||||
```
|
||||
|
||||
**Connection status flow:**
|
||||
1. On mount + 400ms: start typing animation (existing)
|
||||
2. After ~2000ms: `connectionState` transitions to `'connected'`
|
||||
3. Login button is disabled until BOTH `typingComplete` AND `connectionState === 'connected'`
|
||||
4. On login click: `isLoading = true`, show "System loading..." state for ~600ms, then `onComplete()`
|
||||
|
||||
**Connection indicator JSX (below login button, above footer):**
|
||||
```tsx
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '12px' }}>
|
||||
<div style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: connectionState === 'connected' ? 'var(--success)' : 'var(--alert)',
|
||||
transition: 'background-color 300ms ease-out',
|
||||
}} />
|
||||
<span style={{ fontSize: '10px', fontFamily: 'var(--font-geist-mono)', color: 'var(--text-tertiary)' }}>
|
||||
{connectionState === 'connected' ? 'Secure connection established' : 'Awaiting secure connection...'}
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Loading state (replaces card content after click):**
|
||||
```tsx
|
||||
{isLoading && (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="loading-spinner" /> {/* CSS animated spinner */}
|
||||
<span style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
|
||||
Loading clinical records...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
**Colour changes throughout LoginScreen:**
|
||||
- `#005EB8` → `#0D6E6E` (accent colour for shield icon bg, active field border, cursor, button)
|
||||
- `#004D9F` → `#0A8080` (button hover)
|
||||
- `#004494` → `#085858` (button pressed)
|
||||
- Background: `#1E293B` → keep as-is or lighten slightly to `#1A2B2A` (matches `--text-primary`)
|
||||
|
||||
---
|
||||
|
||||
### 3.4 CoreSkillsTile — Modifications (now full-width, categorised)
|
||||
|
||||
**Changes:**
|
||||
1. **Full width** (add `full` prop to Card)
|
||||
2. **Categorised display** with 3 groups: Technical, Healthcare Domain, Strategic & Leadership
|
||||
3. **Show top 3-5 per category** on the dashboard
|
||||
4. **"View all" button** triggers detail panel with full list
|
||||
5. **Individual skill click** → detail panel for that skill
|
||||
|
||||
**New internal structure:**
|
||||
```tsx
|
||||
<Card full tileId="core-skills">
|
||||
<CardHeader dotColor="amber" title="REPEAT MEDICATIONS" rightText="Active prescriptions" />
|
||||
|
||||
{/* Category tabs or grouped sections */}
|
||||
{categories.map(category => (
|
||||
<div key={category.id}>
|
||||
<CategoryHeader label={category.label} count={category.skills.length} />
|
||||
{category.skills.slice(0, 4).map(skill => (
|
||||
<SkillItem
|
||||
key={skill.id}
|
||||
skill={skill}
|
||||
onClick={() => openPanel({ type: 'skill', skill })}
|
||||
/>
|
||||
))}
|
||||
{category.skills.length > 4 && (
|
||||
<ViewMoreButton
|
||||
count={category.skills.length - 4}
|
||||
onClick={() => openPanel({ type: 'skills-all', category: category.id })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
```
|
||||
|
||||
**CategoryHeader sub-component (inline):**
|
||||
- Thin divider line with category label
|
||||
- Styled like sidebar section dividers: 10px, uppercase, tertiary, with extending line
|
||||
|
||||
---
|
||||
|
||||
### 3.5 LatestResultsTile — Modifications
|
||||
|
||||
**Changes:**
|
||||
1. **Bigger headline numbers** — increase value font size from 22px to 28-32px
|
||||
2. **Remove flip animation** — replace with click → detail panel
|
||||
3. **Each KPI card is clickable** → `openPanel({ type: 'kpi', kpi })`
|
||||
4. **Visual enhancement:** stronger contrast, bolder presentation
|
||||
|
||||
**KPI card redesign (no more flip):**
|
||||
```tsx
|
||||
<button
|
||||
onClick={() => openPanel({ type: 'kpi', kpi })}
|
||||
className="text-left w-full"
|
||||
style={{
|
||||
padding: '16px',
|
||||
background: 'var(--surface)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
transition: 'border-color 150ms, box-shadow 150ms',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '28px', fontWeight: 700, color: colorMap[kpi.colorVariant] }}>
|
||||
{kpi.value}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', fontWeight: 500, color: 'var(--text-primary)', marginTop: '4px' }}>
|
||||
{kpi.label}
|
||||
</div>
|
||||
<div style={{ fontSize: '10px', fontFamily: 'var(--font-geist-mono)', color: 'var(--text-tertiary)', marginTop: '2px' }}>
|
||||
{kpi.sub}
|
||||
</div>
|
||||
</button>
|
||||
```
|
||||
|
||||
**CSS cleanup:** Remove `.metric-card`, `.metric-card-inner`, `.metric-card-front`, `.metric-card-back` classes from `index.css` (no longer needed once flip is removed).
|
||||
|
||||
---
|
||||
|
||||
### 3.6 ProjectsTile — Modifications (now half-width, card grid)
|
||||
|
||||
**Changes:**
|
||||
1. **Half width** (remove `full` prop) — positioned in right column alongside LatestResults
|
||||
2. **Card grid layout** with thumbnails, title, status, tech tags
|
||||
3. **Click → detail panel (wide)** for full project info
|
||||
4. **Compact display** to fit in half-width tile
|
||||
|
||||
**New layout:**
|
||||
```tsx
|
||||
<Card tileId="projects">
|
||||
<CardHeader dotColor="amber" title="ACTIVE PROJECTS" rightText="Investigations" />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{investigations.map(inv => (
|
||||
<ProjectCard
|
||||
key={inv.id}
|
||||
investigation={inv}
|
||||
onClick={() => openPanel({ type: 'project', investigation: inv })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
```
|
||||
|
||||
**ProjectCard sub-component:**
|
||||
- Compact row: status dot + name + year (right-aligned)
|
||||
- Tech stack as small inline tags
|
||||
- Hover: border colour shift, shadow deepens
|
||||
- Click: opens wide detail panel
|
||||
|
||||
---
|
||||
|
||||
### 3.7 CareerActivityTile — Modifications
|
||||
|
||||
**Changes:**
|
||||
1. **Embed CareerConstellation** component within the tile
|
||||
2. **Timeline items click → detail panel** (instead of in-place accordion)
|
||||
3. **Extended timeline** back to school (2009)
|
||||
4. **Hover preview** on timeline items (slight expand with preview text)
|
||||
|
||||
**New structure:**
|
||||
```tsx
|
||||
<Card full tileId="career-activity">
|
||||
<CardHeader dotColor="teal" title="CAREER ACTIVITY" rightText="Full timeline" />
|
||||
|
||||
{/* Career Constellation D3 graph */}
|
||||
<CareerConstellation
|
||||
onRoleClick={(id) => {
|
||||
const consultation = consultations.find(c => c.id === id)
|
||||
if (consultation) openPanel({ type: 'career-role', consultation })
|
||||
}}
|
||||
onSkillClick={(id) => {
|
||||
const skill = allSkills.find(s => s.id === id)
|
||||
if (skill) openPanel({ type: 'skill', skill })
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Existing timeline below */}
|
||||
<div className="activity-grid" style={{ marginTop: '24px' }}>
|
||||
{/* ... timeline items, now with click → panel instead of accordion */}
|
||||
</div>
|
||||
</Card>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.8 EducationTile — Modifications
|
||||
|
||||
**Changes:**
|
||||
1. **Richer inline content** — show research project score, OSCE score, A-level grades
|
||||
2. **Click → detail panel (narrow)** for full education detail
|
||||
3. Each education entry is a clickable row
|
||||
|
||||
---
|
||||
|
||||
### 3.9 LastConsultationTile — Modifications
|
||||
|
||||
**Changes:**
|
||||
1. **Click → detail panel (wide)** for full role details
|
||||
2. Add a "View full record" link/button at the bottom
|
||||
|
||||
---
|
||||
|
||||
### 3.10 PatientSummaryTile — Modifications
|
||||
|
||||
**Changes:**
|
||||
1. **Content:** Replace current personalStatement with the exact profile text from CV_v4.md
|
||||
2. **Structured presentation:** Consider pulling highlight stats into a visual strip
|
||||
|
||||
The profile.ts data is already the CV_v4.md text, so this may just be a presentation change.
|
||||
|
||||
---
|
||||
|
||||
## 4. Type System Extensions
|
||||
|
||||
### 4.1 New types (`src/types/pmr.ts` additions)
|
||||
|
||||
```typescript
|
||||
// Skill categories for grouped display
|
||||
export type SkillCategory = 'Technical' | 'Domain' | 'Leadership'
|
||||
|
||||
// Extended KPI with story content for detail panel
|
||||
export interface KPIStory {
|
||||
context: string // What this number covers
|
||||
role: string // Your role / what you did
|
||||
outcomes: string[] // Key decisions or results
|
||||
period?: string // Time period
|
||||
}
|
||||
|
||||
// Extended KPI type (augment existing)
|
||||
export interface KPI {
|
||||
id: string
|
||||
value: string
|
||||
label: string
|
||||
sub: string
|
||||
colorVariant: 'green' | 'amber' | 'teal'
|
||||
explanation: string
|
||||
story?: KPIStory // NEW: rich detail for panel
|
||||
}
|
||||
|
||||
// Constellation-specific types
|
||||
export interface ConstellationNode {
|
||||
id: string
|
||||
type: 'role' | 'skill'
|
||||
label: string
|
||||
shortLabel?: string // abbreviated for small nodes
|
||||
organization?: string
|
||||
startYear?: number
|
||||
endYear?: number | null
|
||||
orgColor?: string
|
||||
domain?: 'clinical' | 'technical' | 'leadership'
|
||||
}
|
||||
|
||||
export interface ConstellationLink {
|
||||
source: string
|
||||
target: string
|
||||
strength: number
|
||||
}
|
||||
|
||||
// Detail panel content union
|
||||
export type DetailPanelContent =
|
||||
| { type: 'kpi'; kpi: KPI }
|
||||
| { type: 'skill'; skill: SkillMedication }
|
||||
| { type: 'skills-all'; category?: SkillCategory }
|
||||
| { type: 'consultation'; consultation: Consultation }
|
||||
| { type: 'project'; investigation: Investigation }
|
||||
| { type: 'education'; document: Document }
|
||||
| { type: 'career-role'; consultation: Consultation }
|
||||
|
||||
// Education extras (for detail panel)
|
||||
export interface EducationExtra {
|
||||
documentId: string
|
||||
extracurriculars?: string[]
|
||||
researchDescription?: string
|
||||
programmeDetail?: string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Extensions
|
||||
|
||||
### 5.1 Extended Skills (`src/data/skills.ts`)
|
||||
|
||||
Expand from 5 → ~20 skills across 3 categories. Source: CV_v4.md Core Competencies.
|
||||
|
||||
```typescript
|
||||
// Technical (8 skills)
|
||||
'data-analysis', 'python', 'sql', 'power-bi', 'javascript-typescript',
|
||||
'excel', 'algorithm-design', 'data-pipelines'
|
||||
|
||||
// Healthcare Domain (6 skills)
|
||||
'medicines-optimisation', 'population-health', 'nice-ta',
|
||||
'health-economics', 'clinical-pathways', 'controlled-drugs'
|
||||
|
||||
// Strategic & Leadership (7 skills)
|
||||
'budget-management', 'stakeholder-engagement', 'pharma-negotiation',
|
||||
'team-development', 'change-management', 'financial-modelling', 'executive-comms'
|
||||
```
|
||||
|
||||
Each retains the medication metaphor: frequency, startYear, yearsOfExperience, proficiency, status.
|
||||
|
||||
### 5.2 KPI Stories (`src/data/kpis.ts`)
|
||||
|
||||
Add `story` field to each existing KPI:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: 'budget',
|
||||
value: '£220M',
|
||||
// ... existing fields ...
|
||||
story: {
|
||||
context: 'Total prescribing budget for NHS Norfolk & Waveney ICB, covering primary care prescriptions for a population of 1.2 million across the integrated care system.',
|
||||
role: 'Managed with sophisticated forecasting models, identifying cost pressures and enabling proactive financial planning. Full analytical accountability to ICB board.',
|
||||
outcomes: [
|
||||
'Sophisticated forecasting models identifying cost pressures',
|
||||
'Proactive financial planning enabled across the system',
|
||||
'Interactive dashboard tracking expenditure in real-time',
|
||||
],
|
||||
period: 'Jul 2024 — Present',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Constellation Mapping (`src/data/constellation.ts`)
|
||||
|
||||
New file mapping roles to skills for the D3 graph. Defines which skills connect to which roles.
|
||||
|
||||
### 5.4 Education Extras (`src/data/educationExtras.ts`)
|
||||
|
||||
New file with expanded detail for the education detail panel:
|
||||
|
||||
```typescript
|
||||
export const educationExtras: EducationExtra[] = [
|
||||
{
|
||||
documentId: 'doc-mpharm',
|
||||
extracurriculars: [
|
||||
'President of UEA Pharmacy Society',
|
||||
'Secretary & Vice-President of UEA Ultimate Frisbee',
|
||||
'Publicity Officer for UEA Alzheimer\'s Society',
|
||||
],
|
||||
researchDescription: 'Final year research project investigating cocrystal formation for improved drug delivery properties.',
|
||||
},
|
||||
{
|
||||
documentId: 'doc-mary-seacole',
|
||||
programmeDetail: 'Formal NHS leadership qualification providing theoretical grounding in healthcare leadership approaches, change management, and system-level thinking.',
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Detail Panel Content Renderers
|
||||
|
||||
The `DetailPanel` component delegates rendering to content-specific sub-components based on `content.type`:
|
||||
|
||||
### 6.1 KPIDetail (`src/components/detail/KPIDetail.tsx`)
|
||||
- Headline number (large, coloured)
|
||||
- Context paragraph
|
||||
- "Your role" paragraph
|
||||
- Outcome bullets
|
||||
- Period badge
|
||||
|
||||
### 6.2 SkillDetail (`src/components/detail/SkillDetail.tsx`)
|
||||
- Skill name + frequency + status badge
|
||||
- Proficiency bar
|
||||
- Years of experience
|
||||
- Prescribing history timeline (reuse existing pattern from CoreSkillsTile)
|
||||
- "Used in" section: list of roles that used this skill (from constellation mapping)
|
||||
|
||||
### 6.3 SkillsAllDetail (`src/components/detail/SkillsAllDetail.tsx`)
|
||||
- Full categorised list of all skills
|
||||
- Grouped by Technical / Healthcare Domain / Strategic & Leadership
|
||||
- Each skill clickable to switch panel to individual skill detail
|
||||
|
||||
### 6.4 ConsultationDetail (`src/components/detail/ConsultationDetail.tsx`)
|
||||
- Role title + organisation + dates
|
||||
- History paragraph (from `consultation.history`)
|
||||
- Achievement bullets (from `consultation.examination`)
|
||||
- Plan/outcomes (from `consultation.plan`)
|
||||
- Coded entries badges (from `consultation.codedEntries`)
|
||||
- Technical environment list
|
||||
|
||||
### 6.5 ProjectDetail (`src/components/detail/ProjectDetail.tsx`)
|
||||
- Project name + year + status
|
||||
- Methodology description
|
||||
- Tech stack tags
|
||||
- Results bullets
|
||||
- External link button (if available)
|
||||
|
||||
### 6.6 EducationDetail (`src/components/detail/EducationDetail.tsx`)
|
||||
- Title + institution + dates + classification
|
||||
- Research project description (if MPharm)
|
||||
- Extracurricular activities
|
||||
- Programme detail (if Mary Seacole)
|
||||
- Notes
|
||||
|
||||
---
|
||||
|
||||
## 7. Hook Modifications
|
||||
|
||||
### 7.1 `useActiveSection` (existing, to update)
|
||||
|
||||
Currently may observe legacy view IDs. Update to observe the new tile `data-tile-id` attributes and map them to SubNav section IDs:
|
||||
|
||||
```typescript
|
||||
const sectionTileMap: Record<string, string> = {
|
||||
'patient-summary': 'overview',
|
||||
'core-skills': 'skills',
|
||||
'career-activity': 'experience',
|
||||
'projects': 'projects',
|
||||
'education': 'education',
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 `useFocusTrap` (new hook, `src/hooks/useFocusTrap.ts`)
|
||||
|
||||
For the DetailPanel. Traps Tab key focus within the panel when open.
|
||||
|
||||
```typescript
|
||||
export function useFocusTrap(containerRef: RefObject<HTMLElement>, isActive: boolean): void
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. New Dependency
|
||||
|
||||
```bash
|
||||
npm install d3 @types/d3
|
||||
```
|
||||
|
||||
Only `d3-force`, `d3-selection`, `d3-scale`, `d3-transition` are needed. Can import selectively:
|
||||
```typescript
|
||||
import { forceSimulation, forceManyBody, forceLink, forceX, forceY, forceCollide } from 'd3-force'
|
||||
import { select } from 'd3-selection'
|
||||
import { scaleLinear } from 'd3-scale'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. CSS Additions (`src/index.css`)
|
||||
|
||||
```css
|
||||
/* Sub-nav bar */
|
||||
--subnav-height: 36px;
|
||||
|
||||
/* Detail panel */
|
||||
--panel-narrow: 400px;
|
||||
--panel-wide: 60vw;
|
||||
--backdrop-blur: 4px;
|
||||
--backdrop-bg: rgba(26,43,42,0.15);
|
||||
|
||||
/* Detail panel slide animation */
|
||||
@keyframes panel-slide-in {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes panel-slide-out {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
@keyframes backdrop-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes panel-slide-in { from { transform: none; } to { transform: none; } }
|
||||
@keyframes panel-slide-out { from { transform: none; } to { transform: none; } }
|
||||
@keyframes backdrop-fade-in { from { opacity: 1; } to { opacity: 1; } }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Implementation Phases
|
||||
|
||||
### Phase 1: Core Infrastructure
|
||||
1. `DetailPanelContext` + `DetailPanel` component
|
||||
2. `SubNav` component + `useActiveSection` update
|
||||
3. `DashboardLayout` restructure (new tile order, SubNav, DetailPanel)
|
||||
4. `useFocusTrap` hook
|
||||
5. CSS additions (panel animations, sub-nav height)
|
||||
|
||||
### Phase 2: Tile Depth (iterative, per tile)
|
||||
6. `LatestResultsTile` — remove flip, bigger numbers, panel trigger
|
||||
7. `CoreSkillsTile` — full width, categorised, expanded data, "view all"
|
||||
8. `ProjectsTile` — half width, card grid, panel trigger
|
||||
9. `LastConsultationTile` — panel trigger
|
||||
10. `CareerActivityTile` — timeline items → panel, hover preview
|
||||
11. `EducationTile` — richer content, panel trigger
|
||||
12. `PatientSummaryTile` — structured presentation
|
||||
|
||||
### Phase 3: Detail Panel Content
|
||||
13. `KPIDetail` renderer + KPI stories data
|
||||
14. `ConsultationDetail` renderer
|
||||
15. `ProjectDetail` renderer
|
||||
16. `SkillDetail` + `SkillsAllDetail` renderers
|
||||
17. `EducationDetail` renderer + extras data
|
||||
18. Update CommandPalette actions to use detail panel
|
||||
|
||||
### Phase 4: Career Constellation
|
||||
19. Install d3, create `constellation.ts` data mapping
|
||||
20. Build `CareerConstellation` component (D3 force graph)
|
||||
21. Integrate into `CareerActivityTile`
|
||||
22. Hover/click interactions → detail panel
|
||||
23. Accessibility (keyboard nav, screen reader, reduced-motion)
|
||||
|
||||
### Phase 5: Login Refresh
|
||||
24. Visual restyle (teal accents, fonts, shadows)
|
||||
25. Username change to `a.recruiter`
|
||||
26. Connection status indicator (red → green dot)
|
||||
27. Post-login loading state
|
||||
28. TopBar session name update
|
||||
|
||||
### Phase 6: Polish
|
||||
29. Responsive testing (mobile: full-width panels, collapsed sub-nav)
|
||||
30. `prefers-reduced-motion` audit across all new components
|
||||
31. Command palette updates for new content/actions
|
||||
32. Search index update for expanded skills data
|
||||
|
||||
---
|
||||
|
||||
## 11. File Inventory
|
||||
|
||||
### New Files (13)
|
||||
```
|
||||
src/contexts/DetailPanelContext.tsx
|
||||
src/components/DetailPanel.tsx
|
||||
src/components/SubNav.tsx
|
||||
src/components/CareerConstellation.tsx
|
||||
src/components/detail/KPIDetail.tsx
|
||||
src/components/detail/SkillDetail.tsx
|
||||
src/components/detail/SkillsAllDetail.tsx
|
||||
src/components/detail/ConsultationDetail.tsx
|
||||
src/components/detail/ProjectDetail.tsx
|
||||
src/components/detail/EducationDetail.tsx
|
||||
src/data/constellation.ts
|
||||
src/data/educationExtras.ts
|
||||
src/hooks/useFocusTrap.ts
|
||||
```
|
||||
|
||||
### Modified Files (14)
|
||||
```
|
||||
src/App.tsx — wrap DashboardLayout with DetailPanelProvider
|
||||
src/components/DashboardLayout.tsx — SubNav, tile reorder, DetailPanel render
|
||||
src/components/TopBar.tsx — session name → A.RECRUITER
|
||||
src/components/LoginScreen.tsx — visual refresh, connection status, username
|
||||
src/components/Card.tsx — no changes needed (already supports full prop)
|
||||
src/components/tiles/LatestResultsTile.tsx — remove flip, bigger numbers, panel
|
||||
src/components/tiles/CoreSkillsTile.tsx — full width, categorised, view all
|
||||
src/components/tiles/ProjectsTile.tsx — half width, card grid, panel
|
||||
src/components/tiles/LastConsultationTile.tsx — add panel trigger
|
||||
src/components/tiles/CareerActivityTile.tsx — constellation embed, panel triggers
|
||||
src/components/tiles/EducationTile.tsx — richer content, panel trigger
|
||||
src/components/tiles/PatientSummaryTile.tsx — structured presentation
|
||||
src/data/skills.ts — expand to ~20 skills with categories
|
||||
src/data/kpis.ts — add story fields
|
||||
src/types/pmr.ts — new types
|
||||
src/index.css — new CSS vars, animations
|
||||
src/hooks/useActiveSection.ts — update for new tile IDs
|
||||
src/lib/search.ts — update palette for new panel actions
|
||||
package.json — add d3 dependency
|
||||
```
|
||||
|
||||
### Unchanged (locked)
|
||||
```
|
||||
src/components/BootSequence.tsx
|
||||
src/components/ECGAnimation.tsx
|
||||
```
|
||||
@@ -0,0 +1,300 @@
|
||||
# Requirements Specification: Adding Depth to the GP Clinical Record
|
||||
|
||||
> Brainstorm session output — Feb 2026
|
||||
> Source of truth for content: `References/CV_v4.md`
|
||||
> ATS PDF is supplementary context only, not to be included wholesale.
|
||||
|
||||
---
|
||||
|
||||
## 1. Problem Statement
|
||||
|
||||
The current dashboard feels flat and light on information. Content is thin, sections feel like footnotes rather than showcases, and there's no mechanism to drill into detail. Projects are buried at the bottom. KPI numbers don't hit hard enough. Skills show only 5 items with no way to see more. The whole experience lacks depth, interactivity, and the sense that there's rich content behind every surface.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core UX Patterns
|
||||
|
||||
### 2.1 Right-Side Detail Panel
|
||||
|
||||
A slide-in panel from the right edge of the screen — the primary mechanism for depth.
|
||||
|
||||
- **Trigger:** "View more" buttons, clickable career items, skills, KPIs, projects
|
||||
- **Entrance:** Slides in from the right. Dashboard content blurs slightly behind via backdrop-filter.
|
||||
- **Adaptive width:**
|
||||
- **Narrow (~400px):** For simple items — individual skills, education entries, single KPI stories
|
||||
- **Wide (~60% viewport):** For complex items — career roles with achievement lists, projects with screenshots/outcomes/tech stacks
|
||||
- **Close:** Click outside the panel, press Escape, or click a close button
|
||||
- **Animation:** 250ms ease-out slide, 150ms backdrop blur transition
|
||||
- **Accessibility:** Focus trap when open, Escape to close, ARIA role="dialog"
|
||||
|
||||
### 2.2 Sub-Navigation Bar
|
||||
|
||||
A fixed navigation strip below the TopBar for section jumping.
|
||||
|
||||
- **Position:** Immediately below TopBar, above the card grid content area
|
||||
- **Labels:** `Overview` | `Skills` | `Experience` | `Projects` | `Education`
|
||||
- **Behaviour:**
|
||||
- Click → smooth-scroll to that section
|
||||
- Active tab highlights based on scroll position (IntersectionObserver)
|
||||
- Sticky — stays visible as user scrolls
|
||||
- **Style:** Clean, understated. Matches GP system tab row aesthetic. Teal underline for active tab.
|
||||
|
||||
### 2.3 Hover + Click Interaction Model
|
||||
|
||||
Everything should feel alive and reactive:
|
||||
|
||||
- **Hover:** Items lift slightly — shadow deepens, subtle border colour shift. Cursor changes to pointer.
|
||||
- **Click:** Opens the detail panel with full content for that item.
|
||||
- **Career activity items:** Expand a small amount inline on hover (preview), then full detail via click → panel.
|
||||
|
||||
---
|
||||
|
||||
## 3. Revised Dashboard Layout
|
||||
|
||||
Tile order changes to prioritise what matters. Projects move up to be prominent. Skills get full width above career history.
|
||||
|
||||
```
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
│ TopBar (fixed, 48px) │
|
||||
│ Brand | Search (Ctrl+K) | Session: a.recruiter │
|
||||
├───────────────────────────────────────────────────────┤
|
||||
│ Sub-Nav Bar (fixed/sticky) │
|
||||
│ Overview | Skills | Experience | Projects | Education│
|
||||
├───────────┬───────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ Sidebar │ 1. Patient Summary (full width) │
|
||||
│ (272px) │ CV_v4.md profile text │
|
||||
│ │ │
|
||||
│ Person │ 2. Latest Results | Projects │
|
||||
│ Header │ (KPIs, left) | (card grid, right) │
|
||||
│ │ │
|
||||
│ Tags │ 3. Repeat Medications (full width) │
|
||||
│ │ Categorised skill groups │
|
||||
│ Alerts │ │
|
||||
│ │ 4. Last Consultation (full width) │
|
||||
│ │ │
|
||||
│ │ 5. Career Activity (full width) │
|
||||
│ │ Timeline + Career Constellation │
|
||||
│ │ │
|
||||
│ │ 6. Education (full width) │
|
||||
│ │ │
|
||||
└───────────┴───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Changes from current:
|
||||
- **Projects move from bottom → half-width right column** (row 2, alongside KPIs)
|
||||
- **Skills move from half-width right → full-width** (row 3, above career history)
|
||||
- **Sub-nav bar added** between TopBar and content
|
||||
- **TopBar session info** shows `a.recruiter` post-login
|
||||
|
||||
---
|
||||
|
||||
## 4. Section Requirements
|
||||
|
||||
### 4.1 Patient Summary (Profile)
|
||||
|
||||
**Content source:** The `## Profile` section from `CV_v4.md`:
|
||||
|
||||
> "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..."
|
||||
|
||||
**Presentation:**
|
||||
- Use the full profile paragraph from CV_v4.md
|
||||
- Structured — consider pulling out key highlights (years of experience, population served, budget managed) as a visual strip alongside the narrative
|
||||
- Not a wall of text — break it up with hierarchy
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Latest Results (KPIs) — Left Column
|
||||
|
||||
**Problem:** Current flip cards feel like footnotes. £220M should be a headline.
|
||||
|
||||
**Dashboard display:**
|
||||
- 4 KPI cards with **bold, large headline numbers** — visually dominant
|
||||
- Stronger contrast, larger type than current implementation
|
||||
- Each card is clearly clickable
|
||||
|
||||
**Click → Detail Panel (narrow):**
|
||||
- Opens with the **story behind the number**
|
||||
- Structure per KPI:
|
||||
- Headline number + label
|
||||
- Context: what it covers, scope, significance
|
||||
- Your role: what you did with this number
|
||||
- Key decisions or outcomes
|
||||
- Optional: supporting visual (mini chart, comparison, timeline)
|
||||
|
||||
**KPIs from CV_v4.md:**
|
||||
1. **£220M** — Prescribing budget managed with sophisticated forecasting models
|
||||
2. **£14.6M** — Efficiency programme identified through data analysis; over-target by Oct 2025
|
||||
3. **9+ Years** — Professional experience (Aug 2016–present)
|
||||
4. **1.2M** — Population served (Norfolk & Waveney ICS)
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Projects (Investigations) — Right Column (NEW POSITION)
|
||||
|
||||
**Problem:** Currently buried at the bottom as small expandable items.
|
||||
|
||||
**Dashboard display:**
|
||||
- Card grid with **thumbnails/screenshots**, title, status badge, tech tags
|
||||
- Prominent placement alongside KPIs draws immediate attention
|
||||
- Each card is clickable
|
||||
|
||||
**Click → Detail Panel (wide):**
|
||||
- Full project description
|
||||
- Outcome metrics and results
|
||||
- Tech stack with tags
|
||||
- Live link / GitHub link where available
|
||||
- Screenshot or demo visual
|
||||
|
||||
**Projects from current data:**
|
||||
1. PharMetrics Interactive Platform (2024, Live) — with external link
|
||||
2. Patient Switching Algorithm (2025, Complete)
|
||||
3. Blueteq Generator (2023, Complete)
|
||||
4. CD Monitoring System (2024, Complete)
|
||||
5. Sankey Chart Analysis Tool (2023, Complete)
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Skills / Repeat Medications — Full Width (NEW POSITION)
|
||||
|
||||
**Problem:** Only 5 skills, no categorisation, no view more.
|
||||
|
||||
**Dashboard display:**
|
||||
- **Categorised groups** (like BNF chapters in a real formulary):
|
||||
- **Technical:** Python, SQL, Power BI, JavaScript/TypeScript, Real-world data analysis, Dashboard/tool development, Algorithm design, Data pipeline development
|
||||
- **Healthcare Domain:** Medicines optimisation, Population health analytics, NICE TA implementation, Health economics & outcomes, Clinical pathway development, Controlled drug assurance
|
||||
- **Strategic & Leadership:** Budget management (£220M), Stakeholder engagement, Pharmaceutical negotiation, Team development & training, Change management, Financial scenario modelling, Executive communication
|
||||
- **Display:** Top 3-5 per category visible on the dashboard tile, with medication-style frequency/dosing metaphor
|
||||
- **"View all" button** per category or for the whole section
|
||||
|
||||
**Click → Detail Panel (narrow):**
|
||||
- Full categorised list of all skills
|
||||
- Each skill with: proficiency level, years of experience, frequency metaphor (daily, twice daily, when required, etc.)
|
||||
- Skills are interactive — clicking a skill could show which roles/projects used it
|
||||
|
||||
---
|
||||
|
||||
### 4.5 Last Consultation — Full Width
|
||||
|
||||
**Dashboard display:**
|
||||
- Most recent role with headline info (title, organisation, dates, type)
|
||||
- Brief preview of key achievements (2-3 bullets)
|
||||
|
||||
**Click → Detail Panel (wide):**
|
||||
- Full role description from CV_v4.md
|
||||
- All achievement bullets
|
||||
- Technical environment
|
||||
- Coded entries (if applicable)
|
||||
|
||||
---
|
||||
|
||||
### 4.6 Career Activity + Career Constellation — Full Width
|
||||
|
||||
#### Timeline (existing, enhanced)
|
||||
- Colour-coded timeline: teal (roles), amber (projects), green (certifications), purple (education)
|
||||
- **Extended back to school (2009)** — Highworth Grammar through university through career
|
||||
- Role entries expand slightly on hover with preview text
|
||||
- Click → detail panel for full role information
|
||||
|
||||
#### Career Constellation (NEW — D3.js)
|
||||
- **Embedded within the Career Activity section** as a large visual (not a separate view)
|
||||
- **Force-directed network graph** built with D3.js:
|
||||
- **Role nodes:** Large, positioned chronologically left-to-right
|
||||
- Pre-Registration Pharmacist, Paydens (2015-2016)
|
||||
- Duty Pharmacy Manager, Tesco (2016-2017)
|
||||
- Pharmacy Manager, Tesco (2017-2022)
|
||||
- High-Cost Drugs & Interface Pharmacist, NHS (2022-2024)
|
||||
- Deputy Head, Population Health & Data Analysis, NHS (2024-present)
|
||||
- Interim Head, Population Health & Data Analysis, NHS (2025)
|
||||
- **Skill nodes:** Smaller, orbit around the roles they belong to
|
||||
- Colour-coded by domain: clinical (green), technical (teal), leadership (amber)
|
||||
- **Bridge connections:** Skills spanning multiple roles create visible links between eras
|
||||
- e.g., Python connects Tesco-era self-teaching → NHS data work
|
||||
- e.g., Clinical pathway knowledge bridges community pharmacy → NHS HCD role
|
||||
- **Interactions:**
|
||||
- Hover role → its skill cluster highlights and radiates outward
|
||||
- Hover skill → all roles that used it illuminate, showing the through-line
|
||||
- Click role/skill → detail panel
|
||||
- **Purpose:** Demonstrates D3.js/data-vis capability as portfolio content itself
|
||||
|
||||
---
|
||||
|
||||
### 4.7 Education — Full Width
|
||||
|
||||
**Dashboard display:**
|
||||
- Education entries with more detail than current:
|
||||
1. MPharm (Hons) 2:1 — University of East Anglia, 2011-2015
|
||||
- Research project: Drug delivery & cocrystals, 75.1% (Distinction)
|
||||
- 4th year OSCE: 80%
|
||||
2. Mary Seacole Programme — NHS Leadership Academy, 2018, 78%
|
||||
3. A-Levels — Highworth Grammar School, 2009-2011
|
||||
- Mathematics (A*), Chemistry (B), Politics (C)
|
||||
|
||||
**Click → Detail Panel (narrow):**
|
||||
- Full education detail including extracurriculars (Pharmacy Society President, Ultimate Frisbee VP, Alzheimer's Society)
|
||||
- Research project description
|
||||
- Mary Seacole programme detail (change management, healthcare leadership, system-level thinking)
|
||||
|
||||
---
|
||||
|
||||
## 5. Login Page Refresh
|
||||
|
||||
### 5.1 Visual Overhaul
|
||||
- Restyle the login card to match the GP dashboard aesthetic:
|
||||
- Teal accents (not the current colour scheme)
|
||||
- Elvaro Grotesque font
|
||||
- Refined shadows matching the three-tier system
|
||||
- Warm palette cohesive with the dashboard it leads into
|
||||
- Background should feel like the system's pre-authenticated state
|
||||
|
||||
### 5.2 Username Change
|
||||
- **Username typed:** `a.recruiter` (the recruiter is logging into your clinical records)
|
||||
- **TopBar post-login:** Session shows `a.recruiter` as the logged-in user
|
||||
- Password typing remains as dots
|
||||
|
||||
### 5.3 "Awaiting Secure Connection" Polish
|
||||
- Below the login button: a status indicator area
|
||||
- **Initial state:** Red dot + "Awaiting secure connection..."
|
||||
- **After ~2 seconds:** Dot transitions to green + "Secure connection established"
|
||||
- **Login button** becomes clearly interactive only after the green state (was previously greyed/inactive)
|
||||
- Optional: subtle smart card or security authentication visual cue (e.g., a small chip card icon or lock icon animating)
|
||||
|
||||
### 5.4 Post-Login Transition
|
||||
- On button click: brief "System loading..." state with a clinical-style progress indicator
|
||||
- Slight delay (500-800ms) to feel purposeful
|
||||
- Then dashboard materialises with the existing staggered entrance animation (TopBar → Sidebar → Content)
|
||||
|
||||
---
|
||||
|
||||
## 6. Technical Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Career Constellation | D3.js | Industry standard. Most impressive as portfolio piece. Demonstrates serious data-vis skill. |
|
||||
| Detail Panel | Custom React component | Slide-in panel with backdrop blur. Adaptive width based on content type. |
|
||||
| Sub-Nav | IntersectionObserver + scroll | Scroll-spy for active state, smooth scroll on click. |
|
||||
| Content source | CV_v4.md | Primary source of truth for all factual content. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Content Source Hierarchy
|
||||
|
||||
1. **`References/CV_v4.md`** — Primary source of truth for all roles, dates, achievements, numbers
|
||||
2. **`References/Andy_Charlwood_CV_ATS_Optimised.pdf`** — Supplementary context only. Do NOT include wholesale. Use only when CV_v4.md lacks specific detail.
|
||||
3. **`cv-website` data** — Reference for interactivity patterns and content structure, not content itself
|
||||
|
||||
---
|
||||
|
||||
## 8. What This Specification Does NOT Cover
|
||||
|
||||
Per `/sc:brainstorm` boundaries, this document covers requirements only:
|
||||
|
||||
- **No architecture decisions** — use `/sc:design` for component architecture
|
||||
- **No implementation code** — use `/sc:implement` for building
|
||||
- **No database schemas or API contracts** — N/A (static SPA)
|
||||
- **No technical specifications beyond requirements** — implementation details deferred
|
||||
|
||||
### Recommended Next Steps
|
||||
1. `/sc:design` — Design component architecture for detail panel, sub-nav, constellation
|
||||
2. `/sc:workflow` — Generate implementation task breakdown
|
||||
3. Implementation — Build in phases (core UX patterns → section depth → constellation → login refresh)
|
||||
@@ -59,6 +59,27 @@
|
||||
- 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)
|
||||
@@ -401,3 +422,191 @@
|
||||
**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)
|
||||
|
||||
### Iteration 18 — Task 20: Accessibility audit
|
||||
**Status:** Complete
|
||||
**Changes:**
|
||||
- Updated `src/components/Card.tsx`:
|
||||
- Changed wrapper from `<div>` to `<article>` for semantic HTML (tiles are self-contained content sections)
|
||||
- Added `aria-hidden="true"` to CardHeader colored dot (decorative, text label conveys information)
|
||||
- Updated `src/components/TopBar.tsx`:
|
||||
- Added skip link (href="#main-content") positioned off-screen, visible only on focus
|
||||
- Skip link uses accent background, slides down on focus, slides up on blur
|
||||
- Added `aria-label="Active session information"` to session info container
|
||||
- Updated `src/components/DashboardLayout.tsx`:
|
||||
- Added `id="main-content"` to main element (skip link target)
|
||||
- Updated `src/components/Sidebar.tsx`:
|
||||
- Added `aria-hidden="true"` to status badge pulse dot (decorative, "Open to Opportunities" text label conveys status)
|
||||
- Updated `src/components/tiles/CareerActivityTile.tsx`:
|
||||
- Added `aria-hidden="true"` to colored dots (8px activity type indicators — decorative, activity title conveys information)
|
||||
- Updated `src/components/tiles/ProjectsTile.tsx`:
|
||||
- Added `aria-hidden="true"` to status dots (7px Complete/Ongoing/Live indicators — decorative, project name + year conveys information)
|
||||
- Updated `src/index.css`:
|
||||
- Added global `*:focus-visible` styles (2px accent outline, 2px offset)
|
||||
- Specific focus-visible styles for buttons, role="button", role="option", links (2px accent outline rgba(13,110,110,0.4))
|
||||
- Input/textarea focus-visible with slightly stronger accent (rgba 0.6, 0px offset)
|
||||
- Added `prefers-reduced-motion` override for pulse animation (disables pulse on status badge dot — keeps opacity 1)
|
||||
**Learnings:**
|
||||
- **Semantic HTML audit results:**
|
||||
- ✅ TopBar uses `<header>` element (Task 4)
|
||||
- ✅ Sidebar uses `<aside>` element (Task 5)
|
||||
- ✅ DashboardLayout main uses `<main>` element with aria-label (Task 7)
|
||||
- ✅ All tiles now use `<article>` element (this iteration)
|
||||
- ✅ Command palette uses role="dialog" with aria-modal (Task 18)
|
||||
- **Keyboard navigation audit results:**
|
||||
- ✅ Tab navigates between interactive elements (native browser behavior)
|
||||
- ✅ Enter/Space expand tile items, flip KPI cards, select palette results (Task 16-18)
|
||||
- ✅ Escape closes expanded items and command palette (Task 16-18)
|
||||
- ✅ Ctrl+K opens command palette (Task 18)
|
||||
- ✅ Arrow Up/Down navigate palette results (Task 18)
|
||||
- **ARIA attributes audit results:**
|
||||
- ✅ Command palette search: role="combobox", aria-expanded, aria-controls, aria-autocomplete, aria-activedescendant (Task 18)
|
||||
- ✅ Palette results: role="listbox", each result role="option", aria-selected (Task 18)
|
||||
- ✅ Palette overlay: role="dialog", aria-modal="true", aria-label="Command palette" (Task 18)
|
||||
- ✅ Expandable items: aria-expanded on trigger elements (Task 16)
|
||||
- ✅ KPI flip cards: aria-label describing front/back content, role="button", tabIndex={0} (Task 17)
|
||||
- ✅ Decorative dots: aria-hidden="true" on all colored status/type indicators (this iteration)
|
||||
- ✅ Session info: aria-label="Active session information" (this iteration)
|
||||
- **Focus management audit results:**
|
||||
- ✅ Command palette: focus trap implemented, focus moves to search input on open, returns to trigger on close (Task 18)
|
||||
- ✅ Focus-visible rings: 2px accent outline on all interactive elements (this iteration)
|
||||
- ✅ Skip to content link: only visible on focus, navigates to #main-content (this iteration)
|
||||
- ✅ Tile expansion: focus remains on trigger element (native browser behavior with role="button")
|
||||
- **prefers-reduced-motion audit results:**
|
||||
- ✅ All components check at module scope: `const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches`
|
||||
- ✅ Dashboard entrance (topbar/sidebar/content): duration: 0 when reduced motion (Task 7)
|
||||
- ✅ Tile expansion: duration: 0 when reduced motion (Task 16)
|
||||
- ✅ KPI flip: visibility toggle instead of 3D rotation when reduced motion (Task 17)
|
||||
- ✅ Palette entrance: animations disabled when reduced motion (Task 18)
|
||||
- ✅ Status badge pulse: pulse animation disabled when reduced motion (this iteration)
|
||||
- **Color contrast verification:**
|
||||
- ✅ Accent #0D6E6E on white #FFFFFF: ~5.5:1 (meets AA)
|
||||
- ✅ Primary #1A2B2A on white: ~15:1 (meets AAA)
|
||||
- ✅ Secondary #5B7A78 on white: ~4.6:1 (meets AA for normal text)
|
||||
- ✅ Tertiary #8DA8A5 on white: ~3.0:1 (fails for body text — used only for supplementary labels where information is conveyed elsewhere, per ref spec)
|
||||
- ✅ All status colors (success, amber, alert, purple) meet AA contrast on light backgrounds
|
||||
- **Accessibility pattern established:** aria-hidden="true" on ALL decorative colored dots where text labels provide the same information (per WCAG — color cannot be the sole indicator)
|
||||
- **Skip link pattern:** Positioned off-screen with top: -40px, transitions to top: 0 on focus, creates smooth slide-down effect
|
||||
- **Focus ring pattern:** Consistent 2px accent outline with 2px offset across all interactive elements — creates clear, recognizable focus indication
|
||||
**Quality checks:** typecheck ✓, lint ✓ (1 pre-existing warning), build ✓
|
||||
**Visual review:** Not applicable — accessibility improvements are non-visual (semantic HTML, ARIA, keyboard nav) except for focus rings which should be tested by user
|
||||
|
||||
|
||||
@@ -0,0 +1,773 @@
|
||||
# Implementation Workflow: Adding Depth to the GP Clinical Record
|
||||
|
||||
> Generated: Feb 2026
|
||||
> Source: `Ralph/depth-requirements.md` + `Ralph/depth-design.md`
|
||||
> Prerequisite: Task 21 (cleanup) from current plan should be completed first
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Phase 1: Core Infrastructure
|
||||
T1 ─── Types & CSS foundations
|
||||
T2 ─── DetailPanelContext + DetailPanel component ──── depends on T1
|
||||
T3 ─── useFocusTrap hook ─────────────────────────── depends on T1
|
||||
T4 ─── SubNav + useActiveSection update ──────────── depends on T1
|
||||
T5 ─── DashboardLayout restructure ──────────────── depends on T2, T3, T4
|
||||
|
||||
Phase 2: Data Expansion
|
||||
T6 ─── Expand skills.ts (5 → ~20, categorised) ──── depends on T1
|
||||
T7 ─── Add KPI stories to kpis.ts ────────────────── depends on T1
|
||||
T8 ─── Create constellation.ts data ──────────────── depends on T6
|
||||
T9 ─── Create educationExtras.ts ─────────────────── depends on T1
|
||||
|
||||
Phase 3: Tile Modifications (parallel where possible)
|
||||
T10 ─── LatestResultsTile (bigger numbers, panel) ─── depends on T2, T7
|
||||
T11 ─── CoreSkillsTile (full width, categorised) ──── depends on T2, T6
|
||||
T12 ─── ProjectsTile (half width, card grid) ──────── depends on T2
|
||||
T13 ─── LastConsultationTile (panel trigger) ──────── depends on T2
|
||||
T14 ─── CareerActivityTile (panel triggers, hover) ── depends on T2
|
||||
T15 ─── EducationTile (richer content, panel) ─────── depends on T2, T9
|
||||
T16 ─── PatientSummaryTile (structured presentation)─ depends on T5
|
||||
|
||||
Phase 4: Detail Panel Renderers
|
||||
T17 ─── KPIDetail renderer ────────────────────────── depends on T10
|
||||
T18 ─── ConsultationDetail renderer ───────────────── depends on T13, T14
|
||||
T19 ─── ProjectDetail renderer ────────────────────── depends on T12
|
||||
T20 ─── SkillDetail + SkillsAllDetail renderers ───── depends on T11
|
||||
T21 ─── EducationDetail renderer ──────────────────── depends on T15
|
||||
|
||||
Phase 5: Career Constellation (D3.js)
|
||||
T22 ─── Install d3, scaffold CareerConstellation ──── depends on T8
|
||||
T23 ─── D3 force graph rendering ──────────────────── depends on T22
|
||||
T24 ─── Hover/click interactions → detail panel ───── depends on T23, T18, T20
|
||||
T25 ─── Constellation accessibility ───────────────── depends on T23
|
||||
|
||||
Phase 6: Login Refresh
|
||||
T26 ─── LoginScreen visual restyle ────────────────── independent
|
||||
T27 ─── Username → a.recruiter + connection status ── depends on T26
|
||||
T28 ─── Post-login loading state ──────────────────── depends on T27
|
||||
T29 ─── TopBar session name update ────────────────── depends on T28
|
||||
|
||||
Phase 7: Polish & Integration
|
||||
T30 ─── CommandPalette updates for new content ────── depends on T17-T21
|
||||
T31 ─── Responsive testing (panels, sub-nav) ──────── depends on T5, T2
|
||||
T32 ─── prefers-reduced-motion audit ──────────────── depends on all
|
||||
T33 ─── Final visual review + cleanup ─────────────── depends on all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pre-Flight: Complete Task 21 (Cleanup)
|
||||
|
||||
Before starting depth work, the current plan's final task must be done:
|
||||
|
||||
- [ ] Remove unused old components (PatientBanner, ClinicalSidebar, Breadcrumb, MobileBottomNav, PMRInterface)
|
||||
- [ ] Remove old view files (`src/components/views/*.tsx`)
|
||||
- [ ] Remove old portfolio components (Contact, Education, Experience, FloatingNav, Footer, Hero, Projects, Skills)
|
||||
- [ ] Remove unused hooks (useScrollCondensation if unused)
|
||||
- [ ] Verify no dead imports
|
||||
- [ ] `npm run build` clean
|
||||
|
||||
**Checkpoint:** Clean build with zero unused components.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Core Infrastructure
|
||||
|
||||
### Task 1: Types & CSS Foundations
|
||||
|
||||
**Files:** `src/types/pmr.ts`, `src/index.css`
|
||||
**Effort:** Small
|
||||
|
||||
Add all new TypeScript types and CSS custom properties needed by subsequent tasks.
|
||||
|
||||
**Types to add (`src/types/pmr.ts`):**
|
||||
- `SkillCategory` — `'Technical' | 'Domain' | 'Leadership'`
|
||||
- `KPIStory` — context, role, outcomes[], period
|
||||
- Augment `KPI` with optional `story?: KPIStory`
|
||||
- `ConstellationNode` — id, type, label, domain, org data
|
||||
- `ConstellationLink` — source, target, strength
|
||||
- `DetailPanelContent` — discriminated union (kpi | skill | skills-all | consultation | project | education | career-role)
|
||||
- `EducationExtra` — documentId, extracurriculars, researchDescription, programmeDetail
|
||||
- Add `category?: SkillCategory` field to `SkillMedication`
|
||||
|
||||
**CSS to add (`src/index.css`):**
|
||||
```css
|
||||
--subnav-height: 36px;
|
||||
--panel-narrow: 400px;
|
||||
--panel-wide: 60vw;
|
||||
--backdrop-blur: 4px;
|
||||
--backdrop-bg: rgba(26,43,42,0.15);
|
||||
```
|
||||
|
||||
Plus panel animation keyframes (`panel-slide-in`, `panel-slide-out`, `backdrop-fade-in`) with `prefers-reduced-motion` overrides.
|
||||
|
||||
**Validation:** `npm run typecheck` passes.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: DetailPanelContext + DetailPanel Component
|
||||
|
||||
**New files:** `src/contexts/DetailPanelContext.tsx`, `src/components/DetailPanel.tsx`
|
||||
**Depends on:** T1
|
||||
**Effort:** Medium
|
||||
|
||||
**DetailPanelContext (`src/contexts/DetailPanelContext.tsx`):**
|
||||
- `DetailPanelContextValue`: `{ content, openPanel, closePanel, isOpen }`
|
||||
- `DetailPanelProvider` wraps children, manages state
|
||||
- Width mapping: deterministic from `content.type` (narrow for kpi/skill/education, wide for consultation/project/career-role)
|
||||
- Title mapping: derived from content data
|
||||
|
||||
**DetailPanel (`src/components/DetailPanel.tsx`):**
|
||||
- Full-screen backdrop (`backdrop-filter: blur(4px)`, click to close)
|
||||
- Panel slides from right (`translateX(100%)` → `translateX(0)`, 250ms ease-out)
|
||||
- Adaptive width: `var(--panel-narrow)` or `var(--panel-wide)` based on content type
|
||||
- Header: close button (X, lucide `X` icon) + dot + section title
|
||||
- Scrollable content area renders `{children}` (or delegates to content renderers)
|
||||
- Close triggers: backdrop click, Escape key, X button
|
||||
- `aria-modal="true"`, `role="dialog"`, `aria-labelledby`
|
||||
- Mobile: both widths become 100vw
|
||||
- `prefers-reduced-motion`: instant appear, no slide
|
||||
|
||||
**Integration:** Initially renders placeholder content ("Detail panel for {type}"). Real content renderers come in Phase 4.
|
||||
|
||||
**Validation:** Panel opens/closes correctly with keyboard and mouse. `npm run typecheck` + `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: useFocusTrap Hook
|
||||
|
||||
**New file:** `src/hooks/useFocusTrap.ts`
|
||||
**Depends on:** T1
|
||||
**Effort:** Small
|
||||
|
||||
- `useFocusTrap(containerRef: RefObject<HTMLElement>, isActive: boolean): void`
|
||||
- When active: Tab/Shift+Tab cycle within container, first focusable element receives focus
|
||||
- When deactivated: focus returns to the element that was focused before trap activated
|
||||
- Used by DetailPanel (and already used by CommandPalette — consider if CommandPalette can share this hook)
|
||||
|
||||
**Validation:** Tab cycling confirmed in DetailPanel. Focus returns correctly on close.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: SubNav + useActiveSection Update
|
||||
|
||||
**New file:** `src/components/SubNav.tsx`
|
||||
**Modified file:** `src/hooks/useActiveSection.ts`
|
||||
**Depends on:** T1
|
||||
**Effort:** Medium
|
||||
|
||||
**SubNav component:**
|
||||
- Fixed/sticky below TopBar (`top: 48px`, `z-index: 99`)
|
||||
- 5 sections: Overview | Skills | Experience | Projects | Education
|
||||
- Click → smooth-scroll to `[data-tile-id="${tileId}"]`
|
||||
- Active tab: teal underline (2px), text colour `var(--accent)`
|
||||
- Inactive: `var(--text-secondary)`
|
||||
- Height: 36px, background `var(--surface)`, bottom border
|
||||
- Tabs: 13px, font-weight 500, gap 24px
|
||||
- Teal underline slides with 200ms transition
|
||||
|
||||
**useActiveSection update:**
|
||||
- Observe `data-tile-id` attributes on tile elements
|
||||
- Map tile IDs to section IDs (patient-summary→overview, core-skills→skills, etc.)
|
||||
- Use IntersectionObserver with appropriate thresholds
|
||||
|
||||
**Tile `data-tile-id` attributes:** Ensure each tile's Card has this attribute. May need to add `tileId` prop to Card if not already present.
|
||||
|
||||
**Validation:** Scroll triggers correct active tab. Click scrolls to correct section. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: DashboardLayout Restructure
|
||||
|
||||
**Modified file:** `src/components/DashboardLayout.tsx`
|
||||
**Depends on:** T2, T3, T4
|
||||
**Effort:** Medium
|
||||
|
||||
**Changes:**
|
||||
1. Wrap with `DetailPanelProvider` (in App.tsx or DashboardLayout)
|
||||
2. Add `SubNav` between TopBar and content
|
||||
3. Reorder tiles:
|
||||
- PatientSummaryTile (full width)
|
||||
- LatestResultsTile (half) + ProjectsTile (half) — side by side
|
||||
- CoreSkillsTile (full width) — was half, now full
|
||||
- LastConsultationTile (full width)
|
||||
- CareerActivityTile (full width)
|
||||
- EducationTile (full width)
|
||||
4. Render `DetailPanel` alongside CommandPalette
|
||||
5. Adjust margin-top: `calc(var(--topbar-height) + var(--subnav-height))`
|
||||
6. Add `data-tile-id` attributes to tile wrappers
|
||||
|
||||
**Validation:** Layout renders correctly with new tile order. SubNav visible. Detail panel renders. No visual regressions. `npm run build`.
|
||||
|
||||
**Checkpoint:** Core infrastructure complete. Detail panel opens (with placeholder content), sub-nav works, new tile order in place.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Data Expansion
|
||||
|
||||
### Task 6: Expand Skills Data
|
||||
|
||||
**Modified file:** `src/data/skills.ts`
|
||||
**Depends on:** T1
|
||||
**Effort:** Medium
|
||||
|
||||
Expand from 5 → ~20 skills across 3 categories. Each skill retains the medication metaphor.
|
||||
|
||||
**Categories:**
|
||||
- **Technical (8):** Data Analysis, Python, SQL, Power BI, JavaScript/TypeScript, Excel, Algorithm Design, Data Pipelines
|
||||
- **Healthcare Domain (6):** Medicines Optimisation, Population Health, NICE TA Implementation, Health Economics, Clinical Pathways, Controlled Drugs
|
||||
- **Strategic & Leadership (7):** Budget Management, Stakeholder Engagement, Pharmaceutical Negotiation, Team Development, Change Management, Financial Modelling, Executive Communication
|
||||
|
||||
Each skill: `id`, `name`, `genericName`, `frequency`, `startYear`, `yearsOfExperience`, `status`, `proficiency`, `category`
|
||||
|
||||
**Source:** CV_v4.md Core Competencies section.
|
||||
|
||||
**Validation:** Types check. Existing CoreSkillsTile still renders (it will show all skills or first 5 depending on current implementation).
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Add KPI Stories
|
||||
|
||||
**Modified file:** `src/data/kpis.ts`
|
||||
**Depends on:** T1
|
||||
**Effort:** Small
|
||||
|
||||
Add `story` field to each of the 4 KPIs:
|
||||
|
||||
1. **£220M** — prescribing budget, forecasting models, ICB board accountability
|
||||
2. **£14.6M** — efficiency programme, data analysis identification, over-target by Oct 2025
|
||||
3. **9+ Years** — career span Aug 2016–present, progression narrative
|
||||
4. **1.2M** — population served, Norfolk & Waveney ICS scope
|
||||
|
||||
**Source:** CV_v4.md role descriptions.
|
||||
|
||||
**Validation:** Types check. Existing tile unaffected (story field is optional).
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Create Constellation Data
|
||||
|
||||
**New file:** `src/data/constellation.ts`
|
||||
**Depends on:** T6 (needs skill IDs)
|
||||
**Effort:** Medium
|
||||
|
||||
Define role-skill mapping for the D3 graph:
|
||||
|
||||
- 6 role nodes (Paydens → Tesco Duty → Tesco Manager → NHS HCD → NHS Deputy → NHS Interim)
|
||||
- Skill nodes (from expanded skills data)
|
||||
- Links connecting skills to roles with strength values
|
||||
- Colour assignments: role nodes get org colours, skill nodes get domain colours
|
||||
|
||||
**Validation:** Types check. Data importable.
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Create Education Extras
|
||||
|
||||
**New file:** `src/data/educationExtras.ts`
|
||||
**Depends on:** T1
|
||||
**Effort:** Small
|
||||
|
||||
Expanded detail for education entries:
|
||||
- MPharm: extracurriculars (Pharmacy Society President, Ultimate Frisbee VP, Alzheimer's Society), research project description
|
||||
- Mary Seacole: programme detail (change management, healthcare leadership, system-level thinking)
|
||||
- A-Levels: no extras needed
|
||||
|
||||
**Source:** CV_v4.md Education section.
|
||||
|
||||
**Validation:** Types check. Data importable.
|
||||
|
||||
**Checkpoint:** All data expanded and ready for consumption by tiles and detail renderers.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Tile Modifications
|
||||
|
||||
Tasks in this phase can be done in parallel where dependencies allow.
|
||||
|
||||
### Task 10: LatestResultsTile — Remove Flip, Add Panel
|
||||
|
||||
**Modified file:** `src/components/tiles/LatestResultsTile.tsx`
|
||||
**Modified file:** `src/index.css` (remove flip CSS if dedicated)
|
||||
**Depends on:** T2, T7
|
||||
**Effort:** Medium
|
||||
|
||||
**Changes:**
|
||||
1. Remove CSS perspective flip animation entirely
|
||||
2. Remove `.metric-card`, `.metric-card-inner`, `.metric-card-front`, `.metric-card-back` CSS classes
|
||||
3. Replace with clickable KPI cards:
|
||||
- Headline number at 28-32px, bold (700), coloured by variant
|
||||
- Label at 12px, weight 500
|
||||
- Sub-text at 10px, Geist Mono, tertiary
|
||||
4. Click → `openPanel({ type: 'kpi', kpi })`
|
||||
5. Hover: border colour shift + shadow deepens
|
||||
6. Keyboard: Enter/Space triggers panel
|
||||
|
||||
**Validation:** KPIs display with bigger numbers. Click opens detail panel (placeholder). No flip remnants. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 11: CoreSkillsTile — Full Width, Categorised
|
||||
|
||||
**Modified file:** `src/components/tiles/CoreSkillsTile.tsx`
|
||||
**Depends on:** T2, T6
|
||||
**Effort:** Large
|
||||
|
||||
**Changes:**
|
||||
1. Change from half-width to full-width (`full` prop on Card)
|
||||
2. Display skills grouped by category (Technical, Healthcare Domain, Strategic & Leadership)
|
||||
3. Category headers: thin divider line + label (styled like sidebar section dividers)
|
||||
4. Show top 3-4 skills per category on the dashboard
|
||||
5. "View all" button per category → `openPanel({ type: 'skills-all', category })`
|
||||
6. Individual skill click → `openPanel({ type: 'skill', skill })`
|
||||
7. Retain medication metaphor (frequency, status badge)
|
||||
8. Remove single-expand accordion for skills (replaced by panel interaction)
|
||||
|
||||
**Validation:** Skills display in 3 categories. View all opens panel. Individual click opens panel. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 12: ProjectsTile — Half Width, Card Grid
|
||||
|
||||
**Modified file:** `src/components/tiles/ProjectsTile.tsx`
|
||||
**Depends on:** T2
|
||||
**Effort:** Medium
|
||||
|
||||
**Changes:**
|
||||
1. Change from full-width to half-width (remove `full` prop)
|
||||
2. Position alongside LatestResultsTile in the grid (handled by T5 layout reorder)
|
||||
3. Compact card layout: status dot + name + year (right-aligned)
|
||||
4. Tech stack as small inline tags
|
||||
5. Click → `openPanel({ type: 'project', investigation })`
|
||||
6. Remove in-place expansion (replaced by panel)
|
||||
7. Hover: border shift, shadow deepens
|
||||
|
||||
**Validation:** Projects render in half-width alongside KPIs. Click opens panel. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 13: LastConsultationTile — Panel Trigger
|
||||
|
||||
**Modified file:** `src/components/tiles/LastConsultationTile.tsx`
|
||||
**Depends on:** T2
|
||||
**Effort:** Small
|
||||
|
||||
**Changes:**
|
||||
1. Add "View full record" button/link at the bottom
|
||||
2. Click → `openPanel({ type: 'consultation', consultation })`
|
||||
3. Make the tile header area clickable too
|
||||
4. Keep existing inline content (header info row, achievements preview)
|
||||
|
||||
**Validation:** Click opens panel. Existing content unchanged. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 14: CareerActivityTile — Panel Triggers, Hover
|
||||
|
||||
**Modified file:** `src/components/tiles/CareerActivityTile.tsx`
|
||||
**Depends on:** T2
|
||||
**Effort:** Medium
|
||||
|
||||
**Changes:**
|
||||
1. Timeline items: click → `openPanel({ type: 'career-role', consultation })` (for role entries)
|
||||
2. Remove in-place accordion expansion (replaced by panel)
|
||||
3. Hover preview: items lift slightly on hover, show 1-2 lines of preview text
|
||||
4. Keep colour-coded dots and entry type styling
|
||||
5. Reserve space for CareerConstellation embed (Phase 5)
|
||||
|
||||
**Note:** Extended timeline back to school (2009) — add education entries (Highworth Grammar, UEA) to the timeline data if not already present.
|
||||
|
||||
**Validation:** Click opens panel for role items. Hover shows preview. No accordion. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 15: EducationTile — Richer Content, Panel
|
||||
|
||||
**Modified file:** `src/components/tiles/EducationTile.tsx`
|
||||
**Depends on:** T2, T9
|
||||
**Effort:** Small
|
||||
|
||||
**Changes:**
|
||||
1. Show richer inline content: research project score (75.1%), OSCE score (80%), A-level grades
|
||||
2. Each education entry clickable → `openPanel({ type: 'education', document })`
|
||||
3. Hover: border shift
|
||||
|
||||
**Validation:** Richer content visible. Click opens panel. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 16: PatientSummaryTile — Structured Presentation
|
||||
|
||||
**Modified file:** `src/components/tiles/PatientSummaryTile.tsx`
|
||||
**Depends on:** T5
|
||||
**Effort:** Small
|
||||
|
||||
**Changes:**
|
||||
1. Use full profile paragraph from CV_v4.md (verify `profile.ts` has complete text)
|
||||
2. Pull out key highlights as a visual strip (years of experience, population served, budget)
|
||||
3. Break up wall of text with hierarchy (bold key phrases, structured paragraphs)
|
||||
|
||||
**Validation:** Profile reads well, not a wall of text. Highlight strip visible. `npm run build`.
|
||||
|
||||
**Checkpoint:** All tiles modified. Dashboard shows new layout with panel triggers on all interactive elements. Detail panel opens with placeholder content for each type.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Detail Panel Renderers
|
||||
|
||||
### Task 17: KPIDetail Renderer
|
||||
|
||||
**New file:** `src/components/detail/KPIDetail.tsx`
|
||||
**Depends on:** T10
|
||||
**Effort:** Medium
|
||||
|
||||
**Content:**
|
||||
- Headline number (large, coloured by variant)
|
||||
- Context paragraph (from `kpi.story.context`)
|
||||
- "Your role" paragraph (from `kpi.story.role`)
|
||||
- Outcome bullets (from `kpi.story.outcomes`)
|
||||
- Period badge (from `kpi.story.period`)
|
||||
|
||||
**Wire into DetailPanel:** When `content.type === 'kpi'`, render `<KPIDetail kpi={content.kpi} />`.
|
||||
|
||||
**Validation:** Panel renders full KPI story. Content matches CV_v4.md. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 18: ConsultationDetail Renderer
|
||||
|
||||
**New file:** `src/components/detail/ConsultationDetail.tsx`
|
||||
**Depends on:** T13, T14
|
||||
**Effort:** Medium
|
||||
|
||||
**Content:**
|
||||
- Role title + organisation + dates
|
||||
- History paragraph (from `consultation.history`)
|
||||
- Achievement bullets (from `consultation.examination`)
|
||||
- Plan/outcomes (from `consultation.plan`)
|
||||
- Coded entries badges (from `consultation.codedEntries`)
|
||||
|
||||
**Validation:** Panel renders full role detail. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 19: ProjectDetail Renderer
|
||||
|
||||
**New file:** `src/components/detail/ProjectDetail.tsx`
|
||||
**Depends on:** T12
|
||||
**Effort:** Medium
|
||||
|
||||
**Content:**
|
||||
- Project name + year + status badge
|
||||
- Methodology description
|
||||
- Tech stack tags
|
||||
- Results bullets
|
||||
- External link button (if `investigation.link` exists)
|
||||
|
||||
**Validation:** Panel renders full project detail. External link works. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 20: SkillDetail + SkillsAllDetail Renderers
|
||||
|
||||
**New files:** `src/components/detail/SkillDetail.tsx`, `src/components/detail/SkillsAllDetail.tsx`
|
||||
**Depends on:** T11
|
||||
**Effort:** Medium
|
||||
|
||||
**SkillDetail:**
|
||||
- Skill name + frequency + status badge
|
||||
- Proficiency bar (visual)
|
||||
- Years of experience
|
||||
- "Used in" section: roles that used this skill (from constellation mapping, or hardcoded until T8 data available)
|
||||
|
||||
**SkillsAllDetail:**
|
||||
- Full categorised list grouped by Technical / Domain / Leadership
|
||||
- Each skill row clickable → switches panel to individual SkillDetail
|
||||
- Category headers matching tile styling
|
||||
|
||||
**Validation:** Both renderers work. Skill click within SkillsAll switches to SkillDetail. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 21: EducationDetail Renderer
|
||||
|
||||
**New file:** `src/components/detail/EducationDetail.tsx`
|
||||
**Depends on:** T15
|
||||
**Effort:** Small
|
||||
|
||||
**Content:**
|
||||
- Title + institution + dates + classification
|
||||
- Research project description (if MPharm, from `educationExtras`)
|
||||
- Extracurricular activities (from `educationExtras`)
|
||||
- Programme detail (if Mary Seacole, from `educationExtras`)
|
||||
- Notes from document data
|
||||
|
||||
**Validation:** Panel renders education detail with extras. `npm run build`.
|
||||
|
||||
**Checkpoint:** All detail panel content renderers complete. Every interactive element in the dashboard opens its corresponding rich detail view.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Career Constellation (D3.js)
|
||||
|
||||
### Task 22: Install D3, Scaffold CareerConstellation
|
||||
|
||||
**Modified file:** `package.json` (add `d3`, `@types/d3`)
|
||||
**New file:** `src/components/CareerConstellation.tsx` (scaffold)
|
||||
**Depends on:** T8
|
||||
**Effort:** Small
|
||||
|
||||
- `npm install d3 @types/d3`
|
||||
- Create component with `useRef<SVGSVGElement>` for the SVG container
|
||||
- Render an empty SVG with viewBox, correct container sizing
|
||||
- Import constellation data
|
||||
|
||||
**Validation:** Component renders empty SVG. d3 imports resolve. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 23: D3 Force Graph Rendering
|
||||
|
||||
**Modified file:** `src/components/CareerConstellation.tsx`
|
||||
**Depends on:** T22
|
||||
**Effort:** Large
|
||||
|
||||
**Implement the force-directed graph:**
|
||||
- `d3.forceSimulation` with charge, link, x (chronological), y (centred), collision forces
|
||||
- Role nodes: 24px radius, org colour fill, white text
|
||||
- Skill nodes: 10px radius, domain colour-coded (clinical=green, technical=teal, leadership=amber)
|
||||
- Links: thin lines (1px), `var(--border)`, opacity 0.3
|
||||
- Container: full width of CareerActivityTile, 400px desktop / 300px tablet / 250px mobile
|
||||
- SVG with responsive viewBox
|
||||
- Subtle radial gradient background
|
||||
|
||||
**D3 integration pattern:**
|
||||
- D3 operates imperatively via `useEffect` on the SVG ref
|
||||
- React handles wrapper, D3 handles graph
|
||||
- No React state for node positions (performance)
|
||||
|
||||
**Validation:** Graph renders with nodes and links. Nodes positioned chronologically. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 24: Constellation Interactions → Detail Panel
|
||||
|
||||
**Modified file:** `src/components/CareerConstellation.tsx`
|
||||
**Depends on:** T23, T18, T20
|
||||
**Effort:** Medium
|
||||
|
||||
**Hover interactions:**
|
||||
- Hover role → connected skill nodes scale up, links brighten to `var(--accent)`, non-connected nodes fade to 0.15 opacity
|
||||
- Hover skill → all connected role nodes highlight, link paths illuminate
|
||||
- Tooltip with node name on hover
|
||||
|
||||
**Click interactions:**
|
||||
- Click role → `onRoleClick(id)` → opens ConsultationDetail panel
|
||||
- Click skill → `onSkillClick(id)` → opens SkillDetail panel
|
||||
|
||||
**Validation:** Hover highlighting works correctly. Click opens correct detail panels.
|
||||
|
||||
---
|
||||
|
||||
### Task 25: Constellation Accessibility
|
||||
|
||||
**Modified file:** `src/components/CareerConstellation.tsx`
|
||||
**Depends on:** T23
|
||||
**Effort:** Medium
|
||||
|
||||
- `role="img"` on SVG with `aria-label`
|
||||
- Screen-reader-only text description of graph structure
|
||||
- Keyboard navigation: Tab through role nodes, Enter to open detail
|
||||
- `prefers-reduced-motion`: disable force simulation animation, render static final layout
|
||||
- Focus indicators on nodes when keyboard-navigating
|
||||
|
||||
**Validation:** Screen reader describes graph. Keyboard nav works. Reduced motion shows static layout. `npm run build`.
|
||||
|
||||
**Checkpoint:** Career Constellation complete and integrated into CareerActivityTile. Interactive, accessible, visually impressive.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Login Refresh
|
||||
|
||||
### Task 26: LoginScreen Visual Restyle
|
||||
|
||||
**Modified file:** `src/components/LoginScreen.tsx`
|
||||
**Depends on:** None (independent)
|
||||
**Effort:** Medium
|
||||
|
||||
**Colour changes:**
|
||||
- `#005EB8` → `#0D6E6E` (shield icon bg, active field border, cursor, button)
|
||||
- `#004D9F` → `#0A8080` (button hover)
|
||||
- `#004494` → `#085858` (button pressed)
|
||||
- Background: `#1E293B` → keep or lighten to `#1A2B2A`
|
||||
|
||||
**Typography:**
|
||||
- Ensure Elvaro Grotesque is used (not DM Sans or system defaults)
|
||||
- Shadows should match three-tier system
|
||||
|
||||
**Validation:** Login looks cohesive with dashboard. Teal accents throughout. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 27: Username → a.recruiter + Connection Status
|
||||
|
||||
**Modified file:** `src/components/LoginScreen.tsx`
|
||||
**Depends on:** T26
|
||||
**Effort:** Medium
|
||||
|
||||
**Username change:**
|
||||
- Typed username: `a.recruiter` (not `A.CHARLWOOD`)
|
||||
- Password typing remains as dots
|
||||
|
||||
**Connection status indicator (below login button):**
|
||||
- New state: `ConnectionState = 'connecting' | 'connected'`
|
||||
- Initial: red dot + "Awaiting secure connection..."
|
||||
- After ~2000ms: green dot + "Secure connection established"
|
||||
- Dot: 6px circle, colour transitions with 300ms ease-out
|
||||
- Text: 10px, Geist Mono, tertiary colour
|
||||
- Login button disabled until BOTH `typingComplete` AND `connectionState === 'connected'`
|
||||
|
||||
**Validation:** Username types as `a.recruiter`. Connection dot transitions red→green. Button enables correctly.
|
||||
|
||||
---
|
||||
|
||||
### Task 28: Post-Login Loading State
|
||||
|
||||
**Modified file:** `src/components/LoginScreen.tsx`
|
||||
**Depends on:** T27
|
||||
**Effort:** Small
|
||||
|
||||
- On login click: `isLoading = true`
|
||||
- Card content replaces with: spinner + "Loading clinical records..."
|
||||
- Duration: ~600ms
|
||||
- Then calls `onComplete()` → dashboard materialises
|
||||
|
||||
**Validation:** Brief loading state visible between login click and dashboard. Feels purposeful, not slow.
|
||||
|
||||
---
|
||||
|
||||
### Task 29: TopBar Session Name Update
|
||||
|
||||
**Modified file:** `src/components/TopBar.tsx`
|
||||
**Depends on:** T28
|
||||
**Effort:** Tiny
|
||||
|
||||
- Change session display: `Dr. A.CHARLWOOD` → `A.RECRUITER`
|
||||
- Geist Mono font (should already be the case)
|
||||
|
||||
**Validation:** TopBar shows `A.RECRUITER`. `npm run build`.
|
||||
|
||||
**Checkpoint:** Login flow refreshed with teal aesthetic, recruiter narrative, connection status, and loading state.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Polish & Integration
|
||||
|
||||
### Task 30: CommandPalette Updates
|
||||
|
||||
**Modified file:** `src/components/CommandPalette.tsx`, `src/lib/search.ts`
|
||||
**Depends on:** T17-T21
|
||||
**Effort:** Medium
|
||||
|
||||
- Update search index to include expanded skills (20 skills vs 5)
|
||||
- Add "View [X] detail" actions that open the detail panel directly
|
||||
- Ensure palette results link to panel opens, not just scroll-to-section
|
||||
- Update grouping if new content types warrant it
|
||||
|
||||
**Validation:** Search finds all 20 skills. Selecting a result opens the detail panel. `npm run build`.
|
||||
|
||||
---
|
||||
|
||||
### Task 31: Responsive Testing
|
||||
|
||||
**Modified file:** Various
|
||||
**Depends on:** T5, T2
|
||||
**Effort:** Medium
|
||||
|
||||
- DetailPanel: both `narrow` and `wide` become 100vw on mobile (<768px)
|
||||
- SubNav: test on tablet/mobile (may need horizontal scroll or hamburger)
|
||||
- Constellation: test at 300px/250px heights on smaller screens
|
||||
- Projects + KPIs: stack vertically on mobile (grid fallback)
|
||||
- Touch targets: all interactive elements ≥48px
|
||||
|
||||
**Validation:** Test at 375px, 768px, 1024px, 1440px breakpoints. No overflow, no hidden content.
|
||||
|
||||
---
|
||||
|
||||
### Task 32: prefers-reduced-motion Audit
|
||||
|
||||
**Modified file:** Various
|
||||
**Depends on:** All phases
|
||||
**Effort:** Small
|
||||
|
||||
Verify every new animation respects `prefers-reduced-motion: reduce`:
|
||||
- DetailPanel slide → instant appear
|
||||
- Backdrop fade → instant
|
||||
- SubNav underline transition → instant
|
||||
- Constellation force simulation → static layout
|
||||
- Connection status dot transition → instant
|
||||
- Post-login spinner → static indicator
|
||||
- Hover shadows/borders → can keep (non-motion)
|
||||
|
||||
**Validation:** Enable `prefers-reduced-motion` in browser. No animations visible except hover state changes.
|
||||
|
||||
---
|
||||
|
||||
### Task 33: Final Visual Review + Cleanup
|
||||
|
||||
**Depends on:** All phases
|
||||
**Effort:** Medium
|
||||
|
||||
- Visual review against `References/GPSystemconcept.html` (where applicable)
|
||||
- Content verification against `References/CV_v4.md`
|
||||
- Dead import cleanup
|
||||
- Unused CSS removal (old flip card styles)
|
||||
- Console warning check
|
||||
- `npm run typecheck` — zero errors
|
||||
- `npm run lint` — pass (pre-existing warning OK)
|
||||
- `npm run build` — clean
|
||||
|
||||
**Final checkpoint:** Complete depth enhancement. All features working, accessible, responsive, and polished.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Phase | Tasks | New Files | Modified Files | Effort |
|
||||
|-------|-------|-----------|----------------|--------|
|
||||
| 1. Core Infrastructure | T1-T5 | 3 | 3 | Medium-Large |
|
||||
| 2. Data Expansion | T6-T9 | 2 | 2 | Medium |
|
||||
| 3. Tile Modifications | T10-T16 | 0 | 7 | Large |
|
||||
| 4. Detail Renderers | T17-T21 | 6 | 1 | Medium |
|
||||
| 5. Career Constellation | T22-T25 | 1 | 1 | Large |
|
||||
| 6. Login Refresh | T26-T29 | 0 | 2 | Medium |
|
||||
| 7. Polish | T30-T33 | 0 | Several | Medium |
|
||||
| **Total** | **33 tasks** | **12 new files** | **~16 modified** | |
|
||||
|
||||
### Parallelisation Opportunities
|
||||
|
||||
- **T2, T3, T4** can be built in parallel (all depend only on T1)
|
||||
- **T6, T7, T9** can be built in parallel (all depend only on T1)
|
||||
- **T10-T15** can be built in parallel (all depend on T2 + their data task)
|
||||
- **T17-T21** can be built in parallel (each depends on its tile task)
|
||||
- **T26-T29** (login refresh) is independent of Phases 2-5, can run in parallel
|
||||
|
||||
### Critical Path
|
||||
|
||||
T1 → T2 → T5 → T10 → T17 (shortest path to first visible depth feature)
|
||||
T1 → T6 → T8 → T22 → T23 → T24 (path to constellation)
|
||||
|
||||
### New Dependency
|
||||
|
||||
```bash
|
||||
npm install d3 @types/d3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Use `/sc:implement` or begin manual implementation following this workflow phase by phase.
|
||||
Binary file not shown.
@@ -4,9 +4,10 @@ interface CardProps {
|
||||
children: React.ReactNode
|
||||
full?: boolean // spans both grid columns
|
||||
className?: string
|
||||
tileId?: string // data-tile-id for command palette scroll targeting
|
||||
}
|
||||
|
||||
export function Card({ children, full, className }: CardProps) {
|
||||
export function Card({ children, full, className, tileId }: CardProps) {
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
|
||||
const baseStyles: React.CSSProperties = {
|
||||
@@ -22,14 +23,15 @@ export function Card({ children, full, className }: CardProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
<article
|
||||
style={baseStyles}
|
||||
className={className}
|
||||
data-tile-id={tileId}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ export function CardHeader({ dotColor, title, rightText }: CardHeaderProps) {
|
||||
|
||||
return (
|
||||
<div style={headerStyles}>
|
||||
<div style={dotStyles} />
|
||||
<div style={dotStyles} aria-hidden="true" />
|
||||
<span style={titleStyles}>{title}</span>
|
||||
{rightText && <span style={rightTextStyles}>{rightText}</span>}
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import type { ViewId } from '../types/pmr'
|
||||
import { useAccessibility } from '../contexts/AccessibilityContext'
|
||||
import { buildSearchIndex, groupResultsBySection, type SearchResult } from '../lib/search'
|
||||
import { buildLegacySearchIndex, groupResultsBySection, type SearchResult } from '../lib/search'
|
||||
import type { FuseResult } from 'fuse.js'
|
||||
|
||||
interface NavItem {
|
||||
@@ -55,7 +55,7 @@ export function ClinicalSidebar({ activeView, onViewChange, isTablet = false }:
|
||||
const { focusAfterLoginRef, setExpandedItem } = useAccessibility()
|
||||
|
||||
// Build search index once on mount
|
||||
const searchIndex = useMemo(() => buildSearchIndex(), [])
|
||||
const searchIndex = useMemo(() => buildLegacySearchIndex(), [])
|
||||
|
||||
const handleNavClick = useCallback(
|
||||
(view: ViewId) => {
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
|
||||
import {
|
||||
Search,
|
||||
User,
|
||||
Activity,
|
||||
Monitor,
|
||||
Award,
|
||||
GraduationCap,
|
||||
Zap,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
buildPaletteData,
|
||||
buildSearchIndex,
|
||||
groupBySection,
|
||||
} from '@/lib/search'
|
||||
import type { PaletteItem, PaletteAction, IconColorVariant } from '@/lib/search'
|
||||
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
interface CommandPaletteProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onAction?: (action: PaletteAction) => void
|
||||
}
|
||||
|
||||
// Icon mapping by type
|
||||
const iconByType: Record<string, LucideIcon> = {
|
||||
role: User,
|
||||
skill: Activity,
|
||||
project: Monitor,
|
||||
achievement: Award,
|
||||
edu: GraduationCap,
|
||||
action: Zap,
|
||||
}
|
||||
|
||||
// Color variant → CSS variable mapping for icon containers
|
||||
const iconColorStyles: Record<IconColorVariant, { background: string; color: string }> = {
|
||||
teal: { background: 'var(--accent-light)', color: 'var(--accent)' },
|
||||
green: { background: 'var(--success-light)', color: 'var(--success)' },
|
||||
amber: { background: 'var(--amber-light)', color: 'var(--amber)' },
|
||||
purple: { background: 'rgba(124,58,237,0.08)', color: '#7C3AED' },
|
||||
}
|
||||
|
||||
export function CommandPalette({ isOpen, onClose, onAction }: CommandPaletteProps) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const resultsRef = useRef<HTMLDivElement>(null)
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Build data and search index once
|
||||
const paletteData = useMemo(() => buildPaletteData(), [])
|
||||
const searchIndex = useMemo(() => buildSearchIndex(paletteData), [paletteData])
|
||||
|
||||
// Compute visible items based on query
|
||||
const visibleItems = useMemo(() => {
|
||||
if (!query.trim()) {
|
||||
return paletteData
|
||||
}
|
||||
return searchIndex.search(query).map(result => result.item)
|
||||
}, [query, paletteData, searchIndex])
|
||||
|
||||
// Group visible items by section
|
||||
const groupedResults = useMemo(() => groupBySection(visibleItems), [visibleItems])
|
||||
|
||||
// Flat list for keyboard navigation
|
||||
const flatItems = useMemo(() => {
|
||||
const flat: PaletteItem[] = []
|
||||
for (const group of groupedResults) {
|
||||
for (const item of group.items) {
|
||||
flat.push(item)
|
||||
}
|
||||
}
|
||||
return flat
|
||||
}, [groupedResults])
|
||||
|
||||
// Reset state when opening/closing
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setQuery('')
|
||||
setSelectedIndex(-1)
|
||||
// Focus input on next frame
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus()
|
||||
})
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
// Reset selection when query changes
|
||||
useEffect(() => {
|
||||
setSelectedIndex(-1)
|
||||
}, [query])
|
||||
|
||||
// Global Ctrl+K listener
|
||||
useEffect(() => {
|
||||
function handleGlobalKeyDown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
if (!isOpen) {
|
||||
// Parent controls isOpen, so we need onAction or an onOpen callback
|
||||
// For now, the parent will handle Ctrl+K via its own listener
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleGlobalKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleGlobalKeyDown)
|
||||
}, [isOpen])
|
||||
|
||||
// Execute action for a palette item
|
||||
const executeAction = useCallback((item: PaletteItem) => {
|
||||
onClose()
|
||||
if (onAction) {
|
||||
onAction(item.action)
|
||||
} else {
|
||||
// Fallback: handle link and download actions directly
|
||||
const { action } = item
|
||||
if (action.type === 'link') {
|
||||
window.open(action.url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
}, [onClose, onAction])
|
||||
|
||||
// Keyboard navigation within the palette
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown': {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(prev => {
|
||||
const next = prev + 1
|
||||
return next >= flatItems.length ? 0 : next
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(prev => {
|
||||
const next = prev - 1
|
||||
return next < 0 ? flatItems.length - 1 : next
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'Enter': {
|
||||
e.preventDefault()
|
||||
if (selectedIndex >= 0 && selectedIndex < flatItems.length) {
|
||||
executeAction(flatItems[selectedIndex])
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Escape': {
|
||||
e.preventDefault()
|
||||
onClose()
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [flatItems, selectedIndex, executeAction, onClose])
|
||||
|
||||
// Auto-scroll selected item into view
|
||||
useEffect(() => {
|
||||
if (selectedIndex < 0 || !resultsRef.current) return
|
||||
const selectedEl = resultsRef.current.querySelector(`[data-palette-index="${selectedIndex}"]`)
|
||||
if (selectedEl) {
|
||||
selectedEl.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
}, [selectedIndex])
|
||||
|
||||
// Click on overlay (outside modal) to close
|
||||
const handleOverlayClick = useCallback((e: React.MouseEvent) => {
|
||||
if (e.target === overlayRef.current) {
|
||||
onClose()
|
||||
}
|
||||
}, [onClose])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
// Track flat index across groups
|
||||
let flatIndex = 0
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
onClick={handleOverlayClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Command palette"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(26,43,42,0.45)',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
padding: '8px',
|
||||
paddingTop: 'max(8px, 10vh)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
WebkitBackdropFilter: 'blur(4px)',
|
||||
animation: prefersReducedMotion ? 'none' : 'palette-overlay-in 0.2s ease-out forwards',
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Palette modal */}
|
||||
<div
|
||||
className="w-full max-w-[calc(100vw-16px)] md:max-w-[calc(100vw-32px)] md:w-[580px]"
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 24vh)',
|
||||
background: 'var(--surface)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 20px 60px rgba(26,43,42,0.2), 0 0 0 1px rgba(26,43,42,0.08)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
animation: prefersReducedMotion ? 'none' : 'palette-modal-in 0.2s cubic-bezier(0.4,0,0.2,1) forwards',
|
||||
}}
|
||||
>
|
||||
{/* Search input row */}
|
||||
<div
|
||||
className="px-3 py-3 md:px-[18px] md:py-[14px]"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}
|
||||
>
|
||||
<Search
|
||||
size={18}
|
||||
style={{ color: 'var(--accent)', flexShrink: 0 }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search records, experience, skills..."
|
||||
autoComplete="off"
|
||||
className="font-ui"
|
||||
style={{
|
||||
flex: 1,
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
background: 'transparent',
|
||||
fontSize: '15px',
|
||||
color: 'var(--text-primary)',
|
||||
}}
|
||||
aria-label="Search"
|
||||
aria-activedescendant={
|
||||
selectedIndex >= 0 ? `palette-item-${flatItems[selectedIndex]?.id}` : undefined
|
||||
}
|
||||
role="combobox"
|
||||
aria-expanded="true"
|
||||
aria-controls="palette-results"
|
||||
aria-autocomplete="list"
|
||||
/>
|
||||
<kbd
|
||||
className="font-geist"
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: 'var(--text-tertiary)',
|
||||
background: 'var(--bg-dashboard)',
|
||||
border: '1px solid var(--border)',
|
||||
padding: '2px 7px',
|
||||
borderRadius: '4px',
|
||||
flexShrink: 0,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
ESC
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
{/* Results area */}
|
||||
<div
|
||||
id="palette-results"
|
||||
ref={resultsRef}
|
||||
role="listbox"
|
||||
aria-label="Search results"
|
||||
className="pmr-scrollbar p-2 md:p-[8px]"
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{flatItems.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: '32px 16px',
|
||||
color: 'var(--text-tertiary)',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
No results found for “{query}”
|
||||
</div>
|
||||
) : (
|
||||
groupedResults.map((group) => {
|
||||
const sectionItems = group.items.map((item) => {
|
||||
const currentIndex = flatIndex
|
||||
flatIndex++
|
||||
const isSelected = currentIndex === selectedIndex
|
||||
const IconComponent = iconByType[item.iconType]
|
||||
const colorStyle = iconColorStyles[item.iconVariant]
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
id={`palette-item-${item.id}`}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
data-palette-index={currentIndex}
|
||||
onClick={() => executeAction(item)}
|
||||
onMouseEnter={() => setSelectedIndex(currentIndex)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '9px 10px',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
fontSize: '13px',
|
||||
color: 'var(--text-primary)',
|
||||
background: isSelected ? 'var(--accent-light)' : 'transparent',
|
||||
outline: isSelected ? '1.5px solid var(--accent-border)' : 'none',
|
||||
}}
|
||||
>
|
||||
{/* Icon container */}
|
||||
<div
|
||||
style={{
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
background: colorStyle.background,
|
||||
color: colorStyle.color,
|
||||
}}
|
||||
>
|
||||
{IconComponent && <IconComponent size={14} />}
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 500 }}>{item.title}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'var(--text-tertiary)',
|
||||
marginTop: '1px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{item.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div key={group.section}>
|
||||
{/* Section label */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
color: 'var(--text-tertiary)',
|
||||
padding: '8px 10px 5px',
|
||||
}}
|
||||
>
|
||||
{group.section}
|
||||
</div>
|
||||
{sectionItems}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer with keyboard hints */}
|
||||
<div
|
||||
className="hidden md:flex px-3 py-2 md:px-[18px] md:py-[10px]"
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
borderTop: '1px solid var(--border-light)',
|
||||
fontSize: '11px',
|
||||
color: 'var(--text-tertiary)',
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<Kbd>\u2191</Kbd> <Kbd>\u2193</Kbd> Navigate
|
||||
</span>
|
||||
<span>
|
||||
<Kbd>Enter</Kbd> Select
|
||||
</span>
|
||||
<span>
|
||||
<Kbd>Esc</Kbd> Close
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Small kbd element for the footer
|
||||
function Kbd({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<kbd
|
||||
className="font-geist"
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
background: 'var(--bg-dashboard)',
|
||||
border: '1px solid var(--border)',
|
||||
padding: '1px 5px',
|
||||
borderRadius: '3px',
|
||||
color: 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</kbd>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { TopBar } from './TopBar'
|
||||
import Sidebar from './Sidebar'
|
||||
import { CommandPalette } from './CommandPalette'
|
||||
import { PatientSummaryTile } from './tiles/PatientSummaryTile'
|
||||
import { LatestResultsTile } from './tiles/LatestResultsTile'
|
||||
import { CoreSkillsTile } from './tiles/CoreSkillsTile'
|
||||
@@ -9,6 +10,7 @@ import { LastConsultationTile } from './tiles/LastConsultationTile'
|
||||
import { CareerActivityTile } from './tiles/CareerActivityTile'
|
||||
import { EducationTile } from './tiles/EducationTile'
|
||||
import { ProjectsTile } from './tiles/ProjectsTile'
|
||||
import type { PaletteAction } from '@/lib/search'
|
||||
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
@@ -45,12 +47,63 @@ const contentVariants = {
|
||||
}
|
||||
|
||||
export function DashboardLayout() {
|
||||
const [, setCommandPaletteOpen] = useState(false)
|
||||
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
|
||||
|
||||
const handleSearchClick = () => {
|
||||
setCommandPaletteOpen(true)
|
||||
}
|
||||
|
||||
const handlePaletteClose = useCallback(() => {
|
||||
setCommandPaletteOpen(false)
|
||||
}, [])
|
||||
|
||||
// Global Ctrl+K listener to open command palette
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
setCommandPaletteOpen(prev => !prev)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [])
|
||||
|
||||
// Handle palette actions (scroll to tile, expand item, open link, download)
|
||||
const handlePaletteAction = useCallback((action: PaletteAction) => {
|
||||
switch (action.type) {
|
||||
case 'scroll': {
|
||||
const tileEl = document.querySelector(`[data-tile-id="${action.tileId}"]`)
|
||||
if (tileEl) {
|
||||
tileEl.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'expand': {
|
||||
const tileEl = document.querySelector(`[data-tile-id="${action.tileId}"]`)
|
||||
if (tileEl) {
|
||||
tileEl.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
// Dispatch a custom event that the tile can listen for to expand the item
|
||||
const expandEvent = new CustomEvent('palette-expand', {
|
||||
detail: { tileId: action.tileId, itemId: action.itemId },
|
||||
})
|
||||
document.dispatchEvent(expandEvent)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'link': {
|
||||
window.open(action.url, '_blank', 'noopener,noreferrer')
|
||||
break
|
||||
}
|
||||
case 'download': {
|
||||
// For now, open the CV file or trigger a download
|
||||
// This can be wired to an actual PDF when available
|
||||
window.open('/References/CV_v4.md', '_blank')
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="font-ui"
|
||||
@@ -69,11 +122,12 @@ export function DashboardLayout() {
|
||||
height: 'calc(100vh - var(--topbar-height))',
|
||||
}}
|
||||
>
|
||||
{/* Sidebar — fixed left */}
|
||||
{/* Sidebar — hidden on mobile/tablet, visible on desktop */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={sidebarVariants}
|
||||
className="hidden lg:block"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Sidebar />
|
||||
@@ -81,25 +135,18 @@ export function DashboardLayout() {
|
||||
|
||||
{/* Main content — scrollable card grid */}
|
||||
<motion.main
|
||||
id="main-content"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={contentVariants}
|
||||
aria-label="Dashboard content"
|
||||
className="pmr-scrollbar"
|
||||
className="pmr-scrollbar p-4 pb-8 md:p-6 md:pb-10 lg:px-7 lg:pt-6 lg:pb-10"
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
padding: '24px 28px 40px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||
gap: '16px',
|
||||
}}
|
||||
className="dashboard-grid"
|
||||
>
|
||||
<div className="dashboard-grid">
|
||||
{/* PatientSummaryTile — full width */}
|
||||
<PatientSummaryTile />
|
||||
|
||||
@@ -123,7 +170,12 @@ export function DashboardLayout() {
|
||||
</motion.main>
|
||||
</div>
|
||||
|
||||
{/* Command palette will be rendered here (Task 18) */}
|
||||
{/* Command palette overlay */}
|
||||
<CommandPalette
|
||||
isOpen={commandPaletteOpen}
|
||||
onClose={handlePaletteClose}
|
||||
onAction={handlePaletteAction}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -219,6 +219,7 @@ export default function Sidebar() {
|
||||
background: 'var(--success)',
|
||||
animation: 'pulse 2s infinite',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{patient.badge}</span>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,32 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
{/* Skip to main content link (only visible on focus) */}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="skip-link"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-40px',
|
||||
left: '0',
|
||||
background: 'var(--accent)',
|
||||
color: '#FFFFFF',
|
||||
padding: '8px 16px',
|
||||
textDecoration: 'none',
|
||||
zIndex: 101,
|
||||
borderRadius: '0 0 4px 0',
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.top = '0'
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.top = '-40px'
|
||||
}}
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Home
|
||||
@@ -34,7 +60,7 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
className="font-ui"
|
||||
className="font-ui hidden sm:inline"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
@@ -44,6 +70,17 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
||||
Headhunt Medical Center
|
||||
</span>
|
||||
<span
|
||||
className="font-ui sm:hidden"
|
||||
style={{
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--text-primary)',
|
||||
}}
|
||||
>
|
||||
HMC
|
||||
</span>
|
||||
<span
|
||||
className="hidden md:inline"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 400,
|
||||
@@ -120,7 +157,10 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
||||
</button>
|
||||
|
||||
{/* Session info (right) */}
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<div
|
||||
className="flex items-center gap-2 sm:gap-3 shrink-0"
|
||||
aria-label="Active session information"
|
||||
>
|
||||
<span
|
||||
className="hidden sm:inline"
|
||||
style={{
|
||||
@@ -132,7 +172,7 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
||||
Dr. A.CHARLWOOD
|
||||
</span>
|
||||
<span
|
||||
className="font-geist"
|
||||
className="font-geist hidden xs:inline"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'var(--text-tertiary)',
|
||||
@@ -144,6 +184,19 @@ export function TopBar({ onSearchClick }: TopBarProps) {
|
||||
>
|
||||
Active Session · {currentTime}
|
||||
</span>
|
||||
<span
|
||||
className="font-geist xs:hidden"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'var(--text-tertiary)',
|
||||
background: 'var(--accent-light)',
|
||||
padding: '3px 8px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--accent-border)',
|
||||
}}
|
||||
>
|
||||
{currentTime}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -218,6 +218,7 @@ const ActivityItem: React.FC<ActivityItemProps> = ({ entry, isExpanded, onToggle
|
||||
flexShrink: 0,
|
||||
marginTop: '2px',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div
|
||||
@@ -374,7 +375,7 @@ export const CareerActivityTile: React.FC = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Card full>
|
||||
<Card full tileId="career-activity">
|
||||
<CardHeader dotColor="teal" title="CAREER ACTIVITY" rightText="Full timeline" />
|
||||
|
||||
<div className="activity-grid">
|
||||
|
||||
@@ -233,7 +233,7 @@ export function CoreSkillsTile() {
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card tileId="core-skills">
|
||||
<CardHeader dotColor="amber" title="REPEAT MEDICATIONS" />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{skills.map((skill) => (
|
||||
|
||||
@@ -22,7 +22,7 @@ export function EducationTile() {
|
||||
]
|
||||
|
||||
return (
|
||||
<Card full>
|
||||
<Card full tileId="education">
|
||||
<CardHeader dotColor="purple" title="EDUCATION" />
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
|
||||
@@ -30,7 +30,7 @@ export const LastConsultationTile: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card full>
|
||||
<Card full tileId="last-consultation">
|
||||
<CardHeader dotColor="green" title="LAST CONSULTATION" rightText="Most recent role" />
|
||||
|
||||
{/* Header info row */}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { Card, CardHeader } from '../Card'
|
||||
import { kpis } from '@/data/kpis'
|
||||
import type { KPI } from '@/types/pmr'
|
||||
@@ -11,14 +11,31 @@ const colorMap: Record<KPI['colorVariant'], string> = {
|
||||
|
||||
interface MetricCardProps {
|
||||
kpi: KPI
|
||||
isFlipped: boolean
|
||||
onFlip: (id: string) => void
|
||||
}
|
||||
|
||||
function MetricCard({ kpi }: MetricCardProps) {
|
||||
const cardStyles: React.CSSProperties = {
|
||||
padding: '14px',
|
||||
function MetricCard({ kpi, isFlipped, onFlip }: MetricCardProps) {
|
||||
const handleClick = () => {
|
||||
onFlip(kpi.id)
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onFlip(kpi.id)
|
||||
}
|
||||
}
|
||||
|
||||
const outerStyles: React.CSSProperties = {
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
border: '1px solid var(--border-light)',
|
||||
background: 'var(--bg-dashboard)',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
const innerStyles: React.CSSProperties = {
|
||||
padding: '14px',
|
||||
}
|
||||
|
||||
const valueStyles: React.CSSProperties = {
|
||||
@@ -43,16 +60,48 @@ function MetricCard({ kpi }: MetricCardProps) {
|
||||
marginTop: '4px',
|
||||
}
|
||||
|
||||
const backStyles: React.CSSProperties = {
|
||||
padding: '14px',
|
||||
background: 'var(--accent-light)',
|
||||
fontSize: '12px',
|
||||
color: 'var(--text-secondary)',
|
||||
lineHeight: 1.5,
|
||||
fontFamily: 'var(--font-ui)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={cardStyles} data-kpi-id={kpi.id}>
|
||||
<div style={valueStyles}>{kpi.value}</div>
|
||||
<div style={labelStyles}>{kpi.label}</div>
|
||||
<div style={subStyles}>{kpi.sub}</div>
|
||||
<div
|
||||
className="metric-card"
|
||||
style={outerStyles}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`${kpi.label}: ${kpi.value}. ${isFlipped ? 'Showing explanation. Click to show value.' : 'Click to show explanation.'}`}
|
||||
>
|
||||
<div className={`metric-card-inner${isFlipped ? ' flipped' : ''}`}>
|
||||
<div className="metric-card-front" style={innerStyles}>
|
||||
<div style={valueStyles}>{kpi.value}</div>
|
||||
<div style={labelStyles}>{kpi.label}</div>
|
||||
<div style={subStyles}>{kpi.sub}</div>
|
||||
</div>
|
||||
<div className="metric-card-back" style={backStyles}>
|
||||
{kpi.explanation}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function LatestResultsTile() {
|
||||
const [flippedCardId, setFlippedCardId] = useState<string | null>(null)
|
||||
|
||||
const handleFlip = useCallback((id: string) => {
|
||||
setFlippedCardId((prev) => (prev === id ? null : id))
|
||||
}, [])
|
||||
|
||||
const gridStyles: React.CSSProperties = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
@@ -60,11 +109,16 @@ export function LatestResultsTile() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card tileId="latest-results">
|
||||
<CardHeader dotColor="teal" title="LATEST RESULTS" rightText="Updated May 2025" />
|
||||
<div style={gridStyles}>
|
||||
{kpis.map((kpi) => (
|
||||
<MetricCard key={kpi.id} kpi={kpi} />
|
||||
<MetricCard
|
||||
key={kpi.id}
|
||||
kpi={kpi}
|
||||
isFlipped={flippedCardId === kpi.id}
|
||||
onFlip={handleFlip}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -10,7 +10,7 @@ export function PatientSummaryTile() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card full>
|
||||
<Card full tileId="patient-summary">
|
||||
<CardHeader dotColor="teal" title="PATIENT SUMMARY" />
|
||||
<div style={bodyStyles}>{personalStatement}</div>
|
||||
</Card>
|
||||
|
||||
@@ -85,6 +85,7 @@ function ProjectItem({ project, isExpanded, onToggle }: ProjectItemProps) {
|
||||
marginTop: '4px',
|
||||
animation: isLive ? 'pulse 2s infinite' : undefined,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span style={{ flex: 1 }}>{project.name}</span>
|
||||
<span
|
||||
@@ -257,7 +258,7 @@ export function ProjectsTile() {
|
||||
)
|
||||
|
||||
return (
|
||||
<Card full>
|
||||
<Card full tileId="projects">
|
||||
<CardHeader dotColor="amber" title="ACTIVE PROJECTS" />
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
|
||||
+126
-8
@@ -266,26 +266,144 @@ html {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Dashboard card grid responsive */
|
||||
/* Dashboard card grid responsive — mobile-first */
|
||||
.dashboard-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
/* Tablet: 2 columns on wider screens */
|
||||
@media (min-width: 768px) {
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Activity grid responsive */
|
||||
/* Desktop: maintain 2 columns with generous gap */
|
||||
@media (min-width: 1024px) {
|
||||
.dashboard-grid {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* KPI flip cards */
|
||||
.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;
|
||||
}
|
||||
|
||||
.metric-card-back {
|
||||
transform: rotateY(180deg);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.metric-card-inner {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.metric-card-inner.flipped .metric-card-front {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.metric-card-inner .metric-card-back {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.metric-card-inner.flipped .metric-card-back {
|
||||
visibility: visible;
|
||||
transform: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Activity grid responsive — mobile-first (used in CareerActivityTile) */
|
||||
.activity-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
/* Tablet and up: 2 columns */
|
||||
@media (min-width: 768px) {
|
||||
.activity-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== COMMAND PALETTE ANIMATIONS ===== */
|
||||
@keyframes palette-overlay-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes palette-modal-in {
|
||||
from { transform: scale(0.97) translateY(-8px); opacity: 0; }
|
||||
to { transform: scale(1) translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes palette-overlay-in {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes palette-modal-in {
|
||||
from { transform: none; opacity: 1; }
|
||||
to { transform: none; opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== FOCUS VISIBLE STYLES (WCAG Compliance) ===== */
|
||||
/* Default focus ring for all focusable elements */
|
||||
*:focus-visible {
|
||||
outline: 2px solid rgba(13, 110, 110, 0.4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Button-like interactive elements */
|
||||
button:focus-visible,
|
||||
[role="button"]:focus-visible,
|
||||
[role="option"]:focus-visible {
|
||||
outline: 2px solid rgba(13, 110, 110, 0.4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a:focus-visible {
|
||||
outline: 2px solid rgba(13, 110, 110, 0.4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Inputs and textareas */
|
||||
input:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 2px solid rgba(13, 110, 110, 0.6);
|
||||
outline-offset: 0px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* Disable pulse animation on status badge dot */
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
+309
-67
@@ -1,12 +1,298 @@
|
||||
import Fuse, { type FuseResult } from 'fuse.js'
|
||||
import type { ViewId } from '@/types/pmr'
|
||||
|
||||
// Import all data sources
|
||||
import { consultations } from '@/data/consultations'
|
||||
import { medications } from '@/data/medications'
|
||||
import { problems } from '@/data/problems'
|
||||
import { investigations } from '@/data/investigations'
|
||||
import { documents } from '@/data/documents'
|
||||
import { skills } from '@/data/skills'
|
||||
|
||||
export type PaletteSection = 'Experience' | 'Core Skills' | 'Active Projects' | 'Achievements' | 'Education' | 'Quick Actions'
|
||||
|
||||
export type PaletteAction =
|
||||
| { type: 'scroll'; tileId: string }
|
||||
| { type: 'expand'; tileId: string; itemId: string }
|
||||
| { type: 'link'; url: string }
|
||||
| { type: 'download' }
|
||||
|
||||
export type IconColorVariant = 'teal' | 'green' | 'amber' | 'purple'
|
||||
|
||||
export interface PaletteItem {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
section: PaletteSection
|
||||
iconVariant: IconColorVariant
|
||||
iconType: 'role' | 'skill' | 'project' | 'achievement' | 'edu' | 'action'
|
||||
keywords: string
|
||||
action: PaletteAction
|
||||
}
|
||||
|
||||
// Build the full palette dataset matching the concept HTML structure
|
||||
export function buildPaletteData(): PaletteItem[] {
|
||||
const items: PaletteItem[] = []
|
||||
|
||||
// Experience — matching concept HTML entries
|
||||
const experienceEntries: Array<{ title: string; sub: string; keywords: string; activityId: string }> = [
|
||||
{
|
||||
title: 'Interim Head, Population Health & Data Analysis',
|
||||
sub: 'NHS Norfolk & Waveney ICB \u00b7 2024\u20132025',
|
||||
keywords: 'head interim population health data analysis nhs norfolk waveney icb 2024 2025 latest current',
|
||||
activityId: 'interim-head',
|
||||
},
|
||||
{
|
||||
title: 'Senior Data Analyst \u2014 Medicines Optimisation',
|
||||
sub: 'NHS Norfolk & Waveney ICB \u00b7 2021\u20132024',
|
||||
keywords: 'senior data analyst medicines optimisation nhs norfolk waveney icb 2021 2024',
|
||||
activityId: 'senior-analyst',
|
||||
},
|
||||
{
|
||||
title: 'Prescribing Data Pharmacist',
|
||||
sub: 'NHS Norwich CCG \u00b7 2018\u20132021',
|
||||
keywords: 'prescribing data pharmacist nhs norwich ccg 2018 2021',
|
||||
activityId: 'prescribing-pharmacist',
|
||||
},
|
||||
{
|
||||
title: 'Community Pharmacist',
|
||||
sub: 'Boots UK \u00b7 2016\u20132018',
|
||||
keywords: 'community pharmacist boots uk 2016 2018',
|
||||
activityId: 'community-pharmacist',
|
||||
},
|
||||
]
|
||||
|
||||
experienceEntries.forEach((entry, i) => {
|
||||
items.push({
|
||||
id: `exp-${i}`,
|
||||
title: entry.title,
|
||||
subtitle: entry.sub,
|
||||
section: 'Experience',
|
||||
iconVariant: 'teal',
|
||||
iconType: 'role',
|
||||
keywords: entry.keywords,
|
||||
action: { type: 'expand', tileId: 'career-activity', itemId: entry.activityId },
|
||||
})
|
||||
})
|
||||
|
||||
// Core Skills — from skills.ts, matching concept format with proficiency %
|
||||
const skillDescriptions: Record<string, string> = {
|
||||
'Data Analysis': 'Primary expertise \u00b7 NHS population data',
|
||||
'Python': 'Data pipelines, automation, analytics',
|
||||
'SQL': 'Advanced queries, database migration',
|
||||
'Power BI': 'Dashboard design & deployment',
|
||||
'JavaScript / TypeScript': 'Web development & tooling',
|
||||
}
|
||||
|
||||
skills.forEach((skill) => {
|
||||
items.push({
|
||||
id: `skill-${skill.id}`,
|
||||
title: `${skill.name} \u2014 ${skill.proficiency}%`,
|
||||
subtitle: skillDescriptions[skill.name] ?? `${skill.frequency} \u00b7 Since ${skill.startYear}`,
|
||||
section: 'Core Skills',
|
||||
iconVariant: 'green',
|
||||
iconType: 'skill',
|
||||
keywords: `${skill.name.toLowerCase()} ${skill.proficiency} ${skill.frequency.toLowerCase()}`,
|
||||
action: { type: 'expand', tileId: 'core-skills', itemId: skill.id },
|
||||
})
|
||||
})
|
||||
|
||||
// Active Projects — matching concept HTML entries
|
||||
const projectEntries: Array<{ name: string; sub: string; keywords: string; investigationId: string }> = [
|
||||
{
|
||||
name: '\u00a3220M Prescribing Budget',
|
||||
sub: 'Budget oversight & analytical accountability \u00b7 2024',
|
||||
keywords: '220m prescribing budget oversight analytical accountability 2024',
|
||||
investigationId: 'inv-pharmetrics',
|
||||
},
|
||||
{
|
||||
name: 'SQL Analytics Transformation',
|
||||
sub: 'Legacy migration to modern data stack \u00b7 2025',
|
||||
keywords: 'sql analytics transformation legacy migration modern data stack 2025',
|
||||
investigationId: 'inv-switching-algorithm',
|
||||
},
|
||||
{
|
||||
name: 'Team Data Literacy Programme',
|
||||
sub: 'Upskilling 30+ non-technical staff \u00b7 2024',
|
||||
keywords: 'team data literacy programme upskilling non-technical staff 2024 training',
|
||||
investigationId: 'inv-blueteq-gen',
|
||||
},
|
||||
]
|
||||
|
||||
projectEntries.forEach((entry) => {
|
||||
items.push({
|
||||
id: `proj-${entry.investigationId}`,
|
||||
title: entry.name,
|
||||
subtitle: entry.sub,
|
||||
section: 'Active Projects',
|
||||
iconVariant: 'amber',
|
||||
iconType: 'project',
|
||||
keywords: entry.keywords,
|
||||
action: { type: 'expand', tileId: 'projects', itemId: entry.investigationId },
|
||||
})
|
||||
})
|
||||
|
||||
// Achievements — matching concept HTML entries
|
||||
const achievementEntries: Array<{ title: string; sub: string; keywords: string }> = [
|
||||
{
|
||||
title: '\u00a314.6M Efficiency Savings Identified',
|
||||
sub: 'Data-driven prescribing interventions',
|
||||
keywords: '14.6m efficiency savings identified data-driven prescribing interventions money cost',
|
||||
},
|
||||
{
|
||||
title: '\u00a3220M Budget Oversight',
|
||||
sub: 'Full analytical accountability to ICB board',
|
||||
keywords: '220m budget oversight analytical accountability icb board',
|
||||
},
|
||||
{
|
||||
title: 'Power BI Dashboards for 200+ Users',
|
||||
sub: 'Clinicians & commissioners across ICB',
|
||||
keywords: 'power bi dashboards 200 users clinicians commissioners',
|
||||
},
|
||||
{
|
||||
title: 'Team of 12 Led',
|
||||
sub: 'Cross-functional data & population health',
|
||||
keywords: 'team 12 led cross-functional data population health leadership management',
|
||||
},
|
||||
]
|
||||
|
||||
achievementEntries.forEach((entry, i) => {
|
||||
items.push({
|
||||
id: `ach-${i}`,
|
||||
title: entry.title,
|
||||
subtitle: entry.sub,
|
||||
section: 'Achievements',
|
||||
iconVariant: 'amber',
|
||||
iconType: 'achievement',
|
||||
keywords: entry.keywords,
|
||||
action: { type: 'scroll', tileId: 'latest-results' },
|
||||
})
|
||||
})
|
||||
|
||||
// Education — matching concept HTML entries
|
||||
const educationEntries: Array<{ title: string; sub: string; keywords: string }> = [
|
||||
{
|
||||
title: 'MPharm (Hons) \u2014 2:1',
|
||||
sub: 'University of East Anglia \u00b7 2011\u20132015',
|
||||
keywords: 'mpharm hons 2:1 university east anglia uea 2011 2015 pharmacy degree',
|
||||
},
|
||||
{
|
||||
title: 'GPhC Registration',
|
||||
sub: 'General Pharmaceutical Council \u00b7 August 2016',
|
||||
keywords: 'gphc registration general pharmaceutical council 2016 registered',
|
||||
},
|
||||
{
|
||||
title: 'Power BI Data Analyst Associate',
|
||||
sub: 'Microsoft Certified \u00b7 2023',
|
||||
keywords: 'power bi data analyst associate microsoft certified 2023 certification',
|
||||
},
|
||||
{
|
||||
title: 'Clinical Pharmacy Diploma',
|
||||
sub: 'Professional development \u00b7 2019',
|
||||
keywords: 'clinical pharmacy diploma professional development 2019',
|
||||
},
|
||||
]
|
||||
|
||||
educationEntries.forEach((entry, i) => {
|
||||
items.push({
|
||||
id: `edu-${i}`,
|
||||
title: entry.title,
|
||||
subtitle: entry.sub,
|
||||
section: 'Education',
|
||||
iconVariant: 'purple',
|
||||
iconType: 'edu',
|
||||
keywords: entry.keywords,
|
||||
action: { type: 'scroll', tileId: 'education' },
|
||||
})
|
||||
})
|
||||
|
||||
// Quick Actions
|
||||
const quickActions: Array<{ title: string; sub: string; keywords: string; action: PaletteAction }> = [
|
||||
{
|
||||
title: 'Download CV',
|
||||
sub: 'Export as PDF',
|
||||
keywords: 'download cv export pdf resume',
|
||||
action: { type: 'download' },
|
||||
},
|
||||
{
|
||||
title: 'Send Email',
|
||||
sub: 'andy@charlwood.xyz',
|
||||
keywords: 'send email contact andy charlwood',
|
||||
action: { type: 'link', url: 'mailto:andy@charlwood.xyz' },
|
||||
},
|
||||
{
|
||||
title: 'View LinkedIn',
|
||||
sub: 'Professional profile',
|
||||
keywords: 'view linkedin professional profile social',
|
||||
action: { type: 'link', url: 'https://linkedin.com/in/andycharlwood' },
|
||||
},
|
||||
{
|
||||
title: 'View Projects',
|
||||
sub: 'GitHub & portfolio',
|
||||
keywords: 'view projects github portfolio code repositories',
|
||||
action: { type: 'link', url: 'https://github.com/andycharlwood' },
|
||||
},
|
||||
]
|
||||
|
||||
quickActions.forEach((entry, i) => {
|
||||
items.push({
|
||||
id: `action-${i}`,
|
||||
title: entry.title,
|
||||
subtitle: entry.sub,
|
||||
section: 'Quick Actions',
|
||||
iconVariant: 'teal',
|
||||
iconType: 'action',
|
||||
keywords: entry.keywords,
|
||||
action: entry.action,
|
||||
})
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
// Build a fuse.js search index from palette items
|
||||
export function buildSearchIndex(items: PaletteItem[]): Fuse<PaletteItem> {
|
||||
return new Fuse(items, {
|
||||
keys: [
|
||||
{ name: 'title', weight: 2 },
|
||||
{ name: 'subtitle', weight: 1 },
|
||||
{ name: 'keywords', weight: 1.5 },
|
||||
],
|
||||
threshold: 0.3,
|
||||
includeScore: true,
|
||||
minMatchCharLength: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// Section ordering for grouped display
|
||||
const SECTION_ORDER: PaletteSection[] = [
|
||||
'Experience',
|
||||
'Core Skills',
|
||||
'Active Projects',
|
||||
'Achievements',
|
||||
'Education',
|
||||
'Quick Actions',
|
||||
]
|
||||
|
||||
// Group palette items by section, maintaining defined order
|
||||
export function groupBySection(items: PaletteItem[]): Array<{ section: PaletteSection; items: PaletteItem[] }> {
|
||||
const groups = new Map<PaletteSection, PaletteItem[]>()
|
||||
|
||||
for (const item of items) {
|
||||
const existing = groups.get(item.section)
|
||||
if (existing) {
|
||||
existing.push(item)
|
||||
} else {
|
||||
groups.set(item.section, [item])
|
||||
}
|
||||
}
|
||||
|
||||
return SECTION_ORDER
|
||||
.filter(section => groups.has(section))
|
||||
.map(section => ({ section, items: groups.get(section)! }))
|
||||
}
|
||||
|
||||
// ===== LEGACY EXPORTS =====
|
||||
// Used by ClinicalSidebar.tsx (old component, will be removed in Task 21)
|
||||
|
||||
export interface SearchResult {
|
||||
id: string
|
||||
@@ -17,83 +303,40 @@ export interface SearchResult {
|
||||
score?: number
|
||||
}
|
||||
|
||||
// Build a unified search index from all PMR content
|
||||
export function buildSearchIndex(): Fuse<SearchResult> {
|
||||
/** @deprecated Use buildPaletteData() + buildSearchIndex() instead */
|
||||
export function buildLegacySearchIndex(): Fuse<SearchResult> {
|
||||
const searchableItems: SearchResult[] = []
|
||||
|
||||
// Index consultations (Experience)
|
||||
consultations.forEach(consultation => {
|
||||
searchableItems.push({
|
||||
id: consultation.id,
|
||||
title: consultation.role,
|
||||
section: 'consultations',
|
||||
sectionLabel: 'Experience',
|
||||
highlight: `${consultation.role} at ${consultation.organization} — ${consultation.history}`,
|
||||
})
|
||||
consultations.forEach(c => {
|
||||
searchableItems.push({ id: c.id, title: c.role, section: 'consultations', sectionLabel: 'Experience', highlight: `${c.role} at ${c.organization} — ${c.history}` })
|
||||
})
|
||||
medications.forEach(m => {
|
||||
searchableItems.push({ id: m.id, title: m.name, section: 'medications', sectionLabel: 'Skills', highlight: `${m.name} — ${m.frequency} use since ${m.startYear}` })
|
||||
})
|
||||
problems.forEach(p => {
|
||||
searchableItems.push({ id: p.id, title: p.description, section: 'problems', sectionLabel: 'Achievements', highlight: `[${p.code}] ${p.description} — ${p.narrative}` })
|
||||
})
|
||||
investigations.forEach(inv => {
|
||||
searchableItems.push({ id: inv.id, title: inv.name, section: 'investigations', sectionLabel: 'Projects', highlight: `${inv.name} — ${inv.methodology}` })
|
||||
})
|
||||
documents.forEach(doc => {
|
||||
searchableItems.push({ id: doc.id, title: doc.title, section: 'documents', sectionLabel: 'Education', highlight: `${doc.title} from ${doc.source} (${doc.date})` })
|
||||
})
|
||||
|
||||
// Index medications (Skills)
|
||||
medications.forEach(medication => {
|
||||
searchableItems.push({
|
||||
id: medication.id,
|
||||
title: medication.name,
|
||||
section: 'medications',
|
||||
sectionLabel: 'Skills',
|
||||
highlight: `${medication.name} — ${medication.frequency} use since ${medication.startYear}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Index problems (Achievements)
|
||||
problems.forEach(problem => {
|
||||
searchableItems.push({
|
||||
id: problem.id,
|
||||
title: problem.description,
|
||||
section: 'problems',
|
||||
sectionLabel: 'Achievements',
|
||||
highlight: `[${problem.code}] ${problem.description} — ${problem.narrative}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Index investigations (Projects)
|
||||
investigations.forEach(investigation => {
|
||||
searchableItems.push({
|
||||
id: investigation.id,
|
||||
title: investigation.name,
|
||||
section: 'investigations',
|
||||
sectionLabel: 'Projects',
|
||||
highlight: `${investigation.name} — ${investigation.methodology}`,
|
||||
})
|
||||
})
|
||||
|
||||
// Index documents (Education)
|
||||
documents.forEach(document => {
|
||||
searchableItems.push({
|
||||
id: document.id,
|
||||
title: document.title,
|
||||
section: 'documents',
|
||||
sectionLabel: 'Education',
|
||||
highlight: `${document.title} from ${document.source} (${document.date})`,
|
||||
})
|
||||
})
|
||||
|
||||
// Fuse.js configuration for fuzzy search
|
||||
const fuseOptions = {
|
||||
return new Fuse(searchableItems, {
|
||||
keys: [
|
||||
{ name: 'title', weight: 2 }, // Primary match on title
|
||||
{ name: 'highlight', weight: 1 }, // Secondary match on full text
|
||||
{ name: 'title', weight: 2 },
|
||||
{ name: 'highlight', weight: 1 },
|
||||
],
|
||||
threshold: 0.3, // 0 = exact match, 1 = match anything
|
||||
threshold: 0.3,
|
||||
includeScore: true,
|
||||
minMatchCharLength: 2,
|
||||
}
|
||||
|
||||
return new Fuse(searchableItems, fuseOptions)
|
||||
})
|
||||
}
|
||||
|
||||
// Group search results by section
|
||||
/** @deprecated Use groupBySection() instead */
|
||||
export function groupResultsBySection(results: FuseResult<SearchResult>[]): Map<string, FuseResult<SearchResult>[]> {
|
||||
const grouped = new Map<string, FuseResult<SearchResult>[]>()
|
||||
|
||||
results.forEach(result => {
|
||||
const sectionLabel = result.item.sectionLabel
|
||||
if (!grouped.has(sectionLabel)) {
|
||||
@@ -101,6 +344,5 @@ export function groupResultsBySection(results: FuseResult<SearchResult>[]): Map<
|
||||
}
|
||||
grouped.get(sectionLabel)!.push(result)
|
||||
})
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user