191 lines
18 KiB
Plaintext
191 lines
18 KiB
Plaintext
# Progress Log — Career Constellation Refinement
|
|
# Branch: ralph/constellation-refinement
|
|
# Started: 2026-02-16
|
|
|
|
## Codebase Patterns
|
|
- CareerConstellation.tsx (~868 lines) is a D3 force-directed graph with React overlay buttons for accessibility
|
|
- D3 simulation uses forceSimulation with charge, link, x, y, and collide forces
|
|
- Module-level window.matchMedia reads for prefersReducedMotion and supportsCoarsePointer
|
|
- DashboardLayout manages constellation state: highlightedNodeId, pinnedNodeId via callbacks
|
|
- Work experience data in src/data/consultations.ts, constellation-specific data in src/data/constellation.ts
|
|
- CSS layout: .pathway-columns grid — first column is .chronology-stream (work experience), second is .pathway-graph-sticky (constellation graph)
|
|
- Current grid: minmax(0, 1.85fr) minmax(0, 1fr) at desktop — work experience ~65%, graph ~35%
|
|
- containerHeight prop drives graph height on desktop; on mobile (viewport < 1024px) uses MOBILE_FALLBACK_HEIGHT (360px)
|
|
- Use window.innerWidth for breakpoint checks, not container.clientWidth — the SVG container overflows on mobile
|
|
- Design tokens in index.css :root — use var(--accent), var(--border-light), var(--text-tertiary), etc.
|
|
- SVG shadows: use <filter> with <feDropShadow> in <defs>, apply to <g> groups via .attr('filter', 'url(#filter-id)')
|
|
- Role nodes are pill-shaped rects (ROLE_WIDTH=104, ROLE_HEIGHT=32, ROLE_RX=16) with orgColor badge styling
|
|
- Skill nodes use SKILL_RADIUS_DEFAULT (7) resting, SKILL_RADIUS_ACTIVE (11) highlighted — D3 transitions, not CSS
|
|
- Link lines are <path> elements with quadratic bezier curves — tick handler sets d attr
|
|
- Accessibility buttons are React <button> elements overlaid on SVG at opacity 0, container pointerEvents 'none', buttons 'auto'
|
|
- callbacksRef pattern prevents stale closures — use for all D3→React callbacks
|
|
- Bidirectional highlighting: highlightedNodeId (timeline→graph) and highlightedRoleId (graph→timeline)
|
|
- Force simulation: role forceY ~0.98, charge -120/-55, link distance 72, collision ~52-65px roles
|
|
- applyGraphHighlight is the single source of truth for all visual states (resting, highlighted, dimmed)
|
|
- Resting state values (US-003): skill fill-opacity 0.35, skill label opacity 0.5, link stroke-opacity 0.15, dimmed node opacity 0.15, active skill fill-opacity 0.9
|
|
- Initial D3 rendering values MUST match applyGraphHighlight resting values — initial stroke-opacity, fill-opacity, label opacity are set during node/link creation AND in the highlight function
|
|
- Viewport-proportional scaling: dimensions state includes { width, height, scaleFactor }. D3 effect uses `const sf = isMobile ? 1 : scaleFactor`. All desktop pixel values scaled via Math.round(value * sf)
|
|
- scaleFactor formula: Math.max(1, Math.min(1.6, viewportWidth / 1440)) — 1.0x at ≤1440px, 1.6x at ≥2560px. Only active at ≥1024px viewport
|
|
- Use the d3-viz skill for all D3 rendering stories
|
|
- Consultation entries ordered reverse-chronologically (newest first) — new entries go at the end of the array
|
|
- Constellation role nodes, skill mappings, and links are in constellation.ts — adding nodes there automatically extends yScale domain and screen reader description
|
|
- Mobile accordion (coarse pointer): pinnedNodeId drives both graph highlight AND accordion visibility. Accordion only shows for role-type nodes (not skills)
|
|
- SVG background rect has class `.bg-rect` — used for "tap elsewhere to close" handler on touch devices
|
|
- consultation.orgColor is the source of per-employer colour for cards, dots, borders, and coded entries. Use hexToRgba(orgColor, opacity) for tinted variants
|
|
- hexToRgba(hex, opacity) helper exists in both WorkExperienceSubsection.tsx and DashboardLayout.tsx for converting hex to rgba
|
|
|
|
## 2026-02-16 - US-001
|
|
- Added Duty Pharmacy Manager (2016-2017, Tesco PLC) and Pre-Registration Pharmacist (2015-2016, Paydens Pharmacy) role nodes to constellation.ts
|
|
- Added roleSkillMappings entries for both new roles (5 skills for Duty Pharm Mgr, 3 for Pre-Reg)
|
|
- Added constellationLinks with strength values for both new roles
|
|
- Added consultation entries for both new roles to consultations.ts with examination, plan, and codedEntries
|
|
- Fixed Pharmacy Manager orgColor from '#00897B' (teal) to '#E53935' (Tesco red) in both constellation.ts and consultations.ts
|
|
- Updated role count comment from "4 roles" to "6 roles"
|
|
- Files changed: src/data/constellation.ts, src/data/consultations.ts
|
|
- **Learnings for future iterations:**
|
|
- buildScreenReaderDescription() iterates constellationNodes dynamically — no manual update needed when adding roles
|
|
- The #00897B teal colour in index.css (:root --teal) is a generic design token, NOT the Tesco-specific colour — don't change it
|
|
- Consultation.orgColor must match the constellation node orgColor for visual consistency between graph and cards
|
|
---
|
|
|
|
## 2026-02-16 - US-002
|
|
- Added UEA MPharm (2011-2015, University of East Anglia, orgColor #7B2D8E) education node to constellation.ts
|
|
- Added Highworth A-Levels (2009-2011, Highworth Grammar School, orgColor #9C27B0) education node to constellation.ts
|
|
- Added roleSkillMappings: UEA → medicines-optimisation + data-analysis; Highworth → data-analysis
|
|
- Added constellationLinks with strength values (0.5, 0.3 for UEA; 0.2 for Highworth)
|
|
- Added consultation entries for both education entries to consultations.ts (at end of array, maintaining reverse-chronological order)
|
|
- Education nodes use type 'role' — treated identically by the constellation layout engine
|
|
- Updated role count comment to "6 roles + Education nodes (2)"
|
|
- Files changed: src/data/constellation.ts, src/data/consultations.ts
|
|
- **Learnings for future iterations:**
|
|
- Education entries use type 'role' and follow exact same data shape — no special handling needed
|
|
- yScale domain auto-extends from min/max startYear of role-type nodes, so adding 2009 entries extends the timeline automatically
|
|
- Education entries have deliberately few skill connections (2 for UEA, 1 for Highworth) per design to keep lower timeline clean
|
|
- Consultation entries go at end of array (reverse-chronological: newest first → oldest last)
|
|
---
|
|
|
|
## 2026-02-16 - US-003
|
|
- Increased default skill node fill-opacity from 0.2 to 0.35 (initial render + applyGraphHighlight resting state)
|
|
- Increased default skill label opacity from 0 to 0.5 (labels now partially visible at rest)
|
|
- Increased default link stroke-opacity from 0.08 to 0.15
|
|
- Increased active/highlighted skill fill-opacity from 0.85 to 0.9
|
|
- Changed unconnected node dimming from 0.06 to 0.15 opacity
|
|
- Updated non-active link stroke-opacity in highlighted branch from 0.08 to 0.15
|
|
- Changed .pathway-columns desktop grid from 'minmax(0, 1.15fr) minmax(0, 1.5fr)' to 'minmax(0, 1.85fr) minmax(0, 1fr)' — work experience column now ~65%, constellation ~35%
|
|
- Files changed: src/components/CareerConstellation.tsx, src/index.css
|
|
- Browser verified: skills recognisable at a glance without hovering; work experience column visibly wider; constellation adapts to narrower container without clipping
|
|
- **Learnings for future iterations:**
|
|
- Initial D3 rendering attributes (set during node/link creation) must stay in sync with applyGraphHighlight resting values — there are TWO places to update for each visual property
|
|
- The highlighted branch also has a fallback opacity for non-active links/labels — remember to update those too (3 places total: initial render, resting branch, highlighted branch fallback)
|
|
- The constellation ResizeObserver + containerHeight system handles narrower columns automatically — no explicit graph resize code needed
|
|
---
|
|
|
|
## 2026-02-16 - US-004
|
|
- Added viewport-proportional scaling: scaleFactor = Math.max(1, Math.min(1.6, viewportWidth / 1440))
|
|
- scaleFactor stored in dimensions state alongside width/height, computed in resize useEffect
|
|
- Created local `sf` variable in D3 effect (isMobile ? 1 : scaleFactor) to bypass scaling on mobile
|
|
- Scaled node sizes: ROLE_WIDTH (104→~166), ROLE_HEIGHT (32→~51), ROLE_RX (16→~26), SKILL_RADIUS_DEFAULT (7→~11), SKILL_RADIUS_ACTIVE (11→~18)
|
|
- Scaled font sizes: year labels (10→11 base, scales to ~18), role labels (11→12 base, scales to ~19), skill labels (10→11 base, scales to ~18)
|
|
- Scaled spacing: topPadding, bottomPadding, sidePadding, timelineX, roleGap, skillGap, centroid offsets, seeding radius, rightMargin, skillBottomPadding, label dy offset
|
|
- Scaled force simulation: charge (-120→~-192 role, -55→~-88 skill), link distance (72→~115), collision radius offset (10→~16 role, 16→~26 skill)
|
|
- Scaled accessibility button sizes to match scaled SVG nodes
|
|
- Mobile (< 640px) completely bypasses scaling (sf=1), uses MOBILE_ constants unchanged
|
|
- Files changed: src/components/CareerConstellation.tsx, Ralph/prd.json, Ralph/progress.txt
|
|
- Browser verified at 1440px (sf=1.0, identical to pre-change) and 2560px (sf=1.6, all elements clearly larger and well-proportioned)
|
|
- **Learnings for future iterations:**
|
|
- Store scaleFactor in the dimensions state object, not a separate ref — keeps it synced with width/height changes
|
|
- Use `const sf = isMobile ? 1 : scaleFactor` at top of D3 effect to avoid repeating the mobile guard everywhere
|
|
- Every hardcoded pixel value in the D3 effect that relates to element sizing, spacing, or force params needs sf multiplication on desktop path
|
|
- Math.round() wraps all scaled values to avoid sub-pixel rendering artifacts
|
|
- Accessibility overlay buttons in the React JSX also need scaling — they use base constants directly, not the D3-scoped variables
|
|
---
|
|
|
|
## 2026-02-16 - US-005
|
|
- Changed mouseenter handler: on desktop (supportsCoarsePointer === false), calls applyGraphHighlight(d.id) + onNodeHover(d.id) for hover-to-highlight
|
|
- Changed mouseleave handler: resets to highlightedNodeId ?? null (external timeline state or resting), NOT pinnedNodeId
|
|
- Changed click handler: desktop clicks only fire detail callbacks (onRoleClick/onSkillClick), no pin toggle
|
|
- Touch (coarse pointer) retains tap-to-pin toggle unchanged inside click handler
|
|
- pinnedNodeId state only set/cleared for touch interactions
|
|
- Files changed: src/components/CareerConstellation.tsx
|
|
- Browser verified: hover on "Interim Head" → 12 connected skills at fill-opacity 0.9, 9 dimmed at opacity 0.15; hover off → all reset to resting (fill-opacity 0.35, label opacity 0.5); desktop click → no pin state
|
|
- **Learnings for future iterations:**
|
|
- D3 mouseenter/mouseleave events require dispatchEvent() in Playwright headless — native page.hover() on SVG <g> elements doesn't reliably trigger D3 handlers
|
|
- Role rect fill-opacity 0.12 IS the resting state (initialized at line 384), not a dimmed state — don't confuse with skill resting at 0.35
|
|
- mouseleave should reset to highlightedNodeId (external prop) not pinnedNodeId — on desktop there is no pin, so fallback is null (resting)
|
|
- The supportsCoarsePointer guard at top of each handler cleanly separates desktop/touch paths without duplicating the handler
|
|
---
|
|
|
|
## 2026-02-16 - US-006
|
|
- Added mobile accordion expansion below constellation SVG for role details on tap
|
|
- Accordion shows role title, organisation, duration, and top 3 examination items by default
|
|
- "Show more" button reveals full examination and plan arrays (only appears when >3 examination items)
|
|
- Tapping a different role switches accordion content and auto-collapses "show more" (via useEffect on pinnedNodeId)
|
|
- Tapping the same role again or tapping empty SVG background collapses accordion and resets highlights
|
|
- Added click handler on SVG background rect (`.bg-rect`) to clear pinnedNodeId on coarse pointer
|
|
- Accordion uses Framer Motion AnimatePresence with height 0→auto, 200ms ease-out (matches tile expansion pattern)
|
|
- Accordion hidden entirely on desktop (fine pointer) via supportsCoarsePointer guard
|
|
- Skill node taps do not open accordion — only role nodes (filtered by `n.type === 'role'`)
|
|
- Legend hint text changes to "Tap to explore connections" on coarse pointer devices
|
|
- Files changed: src/components/CareerConstellation.tsx, Ralph/prd.json, Ralph/progress.txt
|
|
- **Learnings for future iterations:**
|
|
- The SVG background rect must have a class (`.bg-rect`) for later selection — D3 event handlers on SVG elements created early in the useEffect can reference functions defined later by selecting the element after the function is defined
|
|
- pinnedNodeId is local to CareerConstellation — it's not passed to DashboardLayout. The accordion relies on this internal state
|
|
- Framer Motion `key` prop on motion.div enables smooth exit→enter transitions when switching between different roles (AnimatePresence exits the old key, enters the new)
|
|
- `accordionShowMore` state must reset on pinnedNodeId change to auto-collapse "show more" when switching roles
|
|
- Not all consultations have >3 examination items — the "Show more" button only renders conditionally, and plan items are only shown when expanded
|
|
- Browser testing for coarse pointer features requires touch emulation — Playwright's default Chromium reports fine pointer, so the accordion won't appear without explicit touch device emulation
|
|
---
|
|
|
|
## 2026-02-16 - US-007
|
|
- Created hexToRgba(hex, opacity) helper function in both WorkExperienceSubsection.tsx and DashboardLayout.tsx
|
|
- WorkExperienceSubsection.tsx: replaced all hardcoded teal/accent colour references with consultation.orgColor:
|
|
- Dot indicator: '#0D6E6E' → consultation.orgColor
|
|
- Highlight background: 'rgba(10,128,128,0.03)' → hexToRgba(orgColor, 0.03)
|
|
- Expanded/highlighted border: 'var(--accent-border)' → hexToRgba(orgColor, 0.2)
|
|
- Hover border: 'var(--accent-border)' → hexToRgba(orgColor, 0.2)
|
|
- Left border on expanded detail: 'var(--accent)' → orgColor
|
|
- Bullet dots: 'var(--accent)' → orgColor at 0.5 opacity
|
|
- Coded entry tags: bg hexToRgba(orgColor, 0.08), text orgColor, border hexToRgba(orgColor, 0.2)
|
|
- "View full record" link: 'var(--accent)' → orgColor, hover uses opacity 0.7 instead of accent-hover
|
|
- DashboardLayout.tsx LastConsultationSubsection: same pattern applied:
|
|
- Highlight border/bg, hover bg, role title, bullet dots, "View full record" link all use consultation.orgColor
|
|
- CardHeader dot for "WORK EXPERIENCE" section title remains teal (unchanged)
|
|
- Files changed: src/components/WorkExperienceSubsection.tsx, src/components/DashboardLayout.tsx, Ralph/prd.json, Ralph/progress.txt
|
|
- Browser verified: NHS roles show blue dots/borders, Tesco roles show red, Paydens shows green, education shows purple. Expanded Tesco card shows red left border, red bullet dots, and red-tinted coded entries
|
|
- **Learnings for future iterations:**
|
|
- consultation.orgColor exists on every Consultation object — it's the single source for per-employer colour throughout the UI
|
|
- hexToRgba(hex, opacity) is needed in both WorkExperienceSubsection.tsx and DashboardLayout.tsx — not extracted to a shared utility since it's a small helper and only used in two files
|
|
- For hover effects on org-coloured links, use opacity change (0.7) instead of a separate --accent-hover variable, since each employer has a different base colour
|
|
- The hover mouseenter/mouseleave pattern using parentElement!.style is used for border/shadow effects — it directly mutates the parent wrapper's inline styles
|
|
---
|
|
|
|
## 2026-02-16 - US-008
|
|
- Re-tuned force simulation parameters for 8 entries (6 roles + 2 education) spanning 2009-2025 in ~35% column
|
|
- Increased MOBILE_FALLBACK_HEIGHT from 380 to 520 — 8 entries over 17 years need more vertical space on mobile
|
|
- Reduced desktop sidePadding from 56*sf to 36*sf — frees horizontal space for skill nodes in narrow column
|
|
- Reduced desktop roleGap from 80*sf to 56*sf — roles sit closer to timeline, more room for skills
|
|
- Reduced desktop skillGap from 40*sf to 28*sf — skills start sooner after role pills
|
|
- Reduced skill centroid offset from 60*sf to 40*sf — skills pulled closer to avoid right-edge overflow
|
|
- Reduced skill seed radius from 50*sf to 35*sf — tighter initial positioning
|
|
- Increased mobile charge: roles -80→-100, skills -35→-45 — stronger repulsion for better separation
|
|
- Increased mobile link distance from 48 to 56 — more space between connected nodes
|
|
- Increased mobile collision padding: roles 6→8, skills 10→14 — better overlap prevention
|
|
- Increased collision iterations from 2 to 3 — more passes for cleaner overlap resolution
|
|
- Increased skill forceX strength from 0.18 to 0.25 — pulls skills more towards center of available space
|
|
- Increased desktop rightMargin from 40*sf to 32*sf — moderate boundary for skill labels
|
|
- Added width-aware skill label truncation: maxLen 12 when SVG width < 500px (vs 16 at wider)
|
|
- Increased mobile topPadding 32→36, bottomPadding 32→40 — breathing room at edges
|
|
- Files changed: src/components/CareerConstellation.tsx, Ralph/prd.json, Ralph/progress.txt
|
|
- Browser verified at 375px: all 8 entries visible, correct chronological order, acceptable overlap for mobile
|
|
- Browser verified at 430px: better horizontal distribution, roles well-positioned
|
|
- Browser verified at 1440px: roles cleanly positioned along timeline, skill labels slightly clipped at right edge (container overflow:hidden), circles fully visible
|
|
- Browser verified at 2560px: excellent distribution, all labels visible, education nodes cleanly isolated at bottom
|
|
- **Learnings for future iterations:**
|
|
- MOBILE_FALLBACK_HEIGHT must scale with the number of timeline entries — 380px was adequate for 4 entries but not for 8
|
|
- At 1440px, the ~340px column is fundamentally narrow for 21 skill nodes + labels. Some label clipping via overflow:hidden is an acceptable trade-off — circles are visible and labels show fully on hover
|
|
- Mobile role positioning drifts 1-2 years from exact position due to collision forces pushing close entries apart (2015-2017 has 3 entries). Chronological order is maintained, which is the priority
|
|
- collision.iterations(3) significantly improves overlap prevention over iterations(2) with 29 total nodes
|
|
- Skill forceX strength 0.25 (up from 0.18) keeps skills more centred in available space without over-constraining them
|
|
- The width < 500 check for skill label truncation targets the narrow desktop column specifically — mobile already uses its own 12-char max
|
|
---
|