Files
portfolio/PROMPT.md
T

8.1 KiB
Raw Blame History

Task: CareerConstellation Overhaul

Refactor, visually improve, and add chronological animation to the CareerConstellation D3 force chart — the centrepiece of the portfolio's Patient Pathway section.

Requirements

Phase 1 — Refactor the Monolith

Decompose src/components/CareerConstellation.tsx (1102 lines) into focused modules:

src/components/constellation/
  CareerConstellation.tsx          -- Orchestrator (< 300 lines)
  MobileAccordion.tsx              -- Mobile tap-to-expand accordion
  ConstellationLegend.tsx          -- Domain legend with node counts
  AccessibleNodeOverlay.tsx        -- Keyboard navigation button overlay
  constants.ts                     -- All magic numbers as named exports
  types.ts                         -- SimNode, SimLink, LayoutParams, local interfaces

src/hooks/
  useForceSimulation.ts            -- D3 simulation lifecycle (setup, forces, tick, cleanup)
  useConstellationHighlight.ts     -- applyGraphHighlight + connectedMap + highlight refs
  useConstellationInteraction.ts   -- Mouse/touch/pin handlers, callback refs
  • Constants extracted (forces, sizes, opacities, durations)
  • Types extracted (SimNode, SimLink, LayoutParams)
  • MobileAccordion extracted as standalone component
  • ConstellationLegend extracted
  • AccessibleNodeOverlay extracted
  • useForceSimulation hook created
  • useConstellationHighlight hook created
  • useConstellationInteraction hook created
  • Orchestrator composed from hooks + sub-components (< 300 lines)
  • All existing behaviour preserved (hover, click, tap, keyboard, mobile, detail panel)
  • npm run lint && npm run typecheck && npm run build passes

Phase 2 — Visual Improvements

Enhance the chart aesthetics while maintaining the PMR design language:

Links:

  • Strength-weighted stroke width at rest: 0.5 + strength * 1.5 (range 0.52px)
  • Domain-colored at rest (very low opacity: 0.08 + strength * 0.12)
  • Improved bezier curves: offset control point by vertical distance (cx = (sx+tx)/2 + (ty-sy)*0.15)
  • On highlight: width 1 + strength * 2, domain color at higher opacity

Skill nodes:

  • Thin domain-colored stroke at rest (stroke-width: 1, stroke-opacity: 0.4)
  • Size encoding by connected role count: baseRadius + roleCount * 0.8
  • On highlight: subtle glow filter (feGaussianBlur, 23px stdDeviation, domain color)

Role nodes:

  • Fill gradient: left-to-right from orgColor@0.08 to orgColor@0.18
  • On highlight: fill-opacity 0.25, stroke-width 2, shadow-md filter

Entry animation (mount, replaced by over-time animation in Phase 3):

  • Timeline guides fade in (200ms)
  • Role nodes slide in from left along connectors (staggered 80ms, 300ms each)
  • Skill nodes scale up from 0 (staggered 30ms, 250ms each)
  • Links draw on via stroke-dashoffset (after source+target visible)
  • Skipped entirely when prefers-reduced-motion

Legend:

  • Domain node counts displayed: "Technical (8) · Clinical (6) · Leadership (7)"

Phase 3 — Over-Time Animation

Build the constellation chronologically from 2009 to present:

Data changes:

  • Modify buildConstellationData() in src/data/timeline.ts to include education entities
  • Education entities appear as nodes on the timeline (use type: 'role' with education styling, or add type: 'education')
  • Update src/types/pmr.ts if new node types are needed
  • Timeline order (oldest first): A-Levels (2009) → MPharm (2011) → Pre-Reg (2015) → Duty Manager (2016) → Pharmacy Manager (2017) → High Cost Drugs (2022) → Deputy Head (2024) → Interim Head (2025)

Animation architecture:

  • Create useTimelineAnimation hook in src/hooks/
  • All nodes present in simulation from start but hidden (opacity: 0) — stable positions, no layout jitter
  • Reveal chronologically: each role/education entity appears, then its skills animate in
  • Skills already visible from earlier roles just get new links (reinforcement pulse: scale 1.3x → 1.0x over 350ms)
  • Uses requestAnimationFrame + timestamp scheduler (not setTimeout chains)
  • Animation state machine in refs: IDLE → PLAYING → PAUSED → HOLDING → RESETTING → loop back to PLAYING
  • Auto-plays on load (after force simulation settles)
  • Loops continuously: hold 3s at end → fade all 400ms → pause 200ms → restart

Visual effects during reveal:

  • Role/education nodes scale from 0 with ease-out-back
  • New skill nodes scale from 0 with ease-out
  • Links draw on via stroke-dashoffset animation
  • Year indicator overlay (top-left of SVG, monospace font, var(--text-tertiary))

Accessibility:

  • prefers-reduced-motion: skip animation entirely, show final state immediately
  • Play/pause button with appropriate aria-label

Phase 4 — Animation + Interaction Integration

Wire the animation to the existing highlight system:

  • Hover/tap pauses animation, applies highlight normally (on visible nodes only)
  • Highlight only operates on revealed nodes — unrevealed nodes stay at opacity 0
  • Multiplicative opacity: animation visibility (0 or target) × highlight emphasis (1.0 or 0.15)
  • Resume animation 800ms after last interaction ends (mouseout / background tap)
  • Explicit pause via button stays paused until user clicks play again
  • Play/pause toggle button (bottom-right of SVG area, subtle styling, larger touch target on mobile)
  • Mobile accordion works during paused state
  • Keyboard navigation works during paused state
  • Click → detail panel works during paused state

Success Criteria

All of the following must be true for LOOP_COMPLETE:

  • npm run lint && npm run typecheck && npm run build passes with zero errors
  • CareerConstellation orchestrator is < 300 lines
  • Education entities (A-Levels, MPharm) appear in the constellation
  • Animation auto-plays on load and loops continuously
  • Network builds chronologically from 2009 through to present
  • Skills accumulate visually — existing skills get new links, not duplicated
  • Hover/tap pauses animation and shows highlight on visible nodes
  • Animation resumes after 800ms of no interaction
  • Play/pause button visible and functional
  • Existing interactions preserved: click → detail panel, keyboard nav, mobile accordion
  • prefers-reduced-motion shows final state immediately with no animation
  • Links show domain colors and strength-weighted width at rest
  • No TypeScript any types introduced
  • No dead code or commented-out blocks

Constraints

  • TypeScript strict mode (noUnusedLocals, noUnusedParameters)
  • Path alias: @/*src/*
  • Styling: Tailwind utilities + CSS custom properties for design tokens
  • D3 v6 (already installed)
  • Framer Motion for non-D3 animations; respect prefers-reduced-motion
  • Design tokens: Primary teal #00897B, Accent coral #FF6B6B, PMR greens/teals/greys
  • Font tokens: --font-ui (Elvaro), --font-geist-mono (monospace), --font-primary / --font-secondary
  • No automated tests — quality gates are lint + typecheck + build
  • D3 patterns: reference .claude/skills/d3-visualization/ for force layout examples

Key Architecture Decisions

  1. "All nodes hidden" for animation — every node participates in the force simulation from the start (positions are stable). Reveal via opacity transitions only. Do NOT dynamically add/remove nodes from the simulation.

  2. Ref-based animation state — the animation state machine lives in refs (not React state) to avoid re-renders in the rAF loop. Only sync to React state for UI controls (play/pause button).

  3. Multiplicative opacity model — animation controls visibility (0 or target), highlight controls emphasis (1.0 or 0.15). Final opacity = animation × highlight. This prevents the two systems from conflicting.

  4. Imperative D3 + React hybrid — D3 manages SVG rendering and force simulation imperatively via refs. React manages keyboard overlay buttons and UI controls. Follow the existing pattern in the codebase.

Status

Track progress here. Mark items complete as you go. When ALL success criteria are met, print LOOP_COMPLETE.