diff --git a/Ralph/prd.json b/Ralph/prd.json index 3f885e1..b8dcafc 100644 --- a/Ralph/prd.json +++ b/Ralph/prd.json @@ -138,7 +138,7 @@ "Verify in browser — links are nearly invisible at rest and clearly trace pathways on hover" ], "priority": 7, - "passes": false, + "passes": true, "notes": "The linkSelection is created at lines ~340-345. Change from .join('line') to .join('path'). For the curve, generate a simple quadratic or cubic bezier path string in the tick handler: given source (sx,sy) and target (tx,ty), create a path like M sx,sy Q cx,cy tx,ty where cx,cy is a control point offset to create a gentle arc. A simple approach: control point at ((sx+tx)/2, sy) or ((sx+tx)/2, (sy+ty)/2 + offset). Alternatively use d3.linkHorizontal() or d3.linkVertical() which generate smooth curves between two points. The applyGraphHighlight function's link styling (lines ~465-482) needs updating from line attributes to path attributes — but stroke/stroke-opacity/stroke-width work the same on paths. Use the d3-viz skill for implementation." }, { diff --git a/Ralph/progress.txt b/Ralph/progress.txt index 3fa3391..db72f55 100644 --- a/Ralph/progress.txt +++ b/Ralph/progress.txt @@ -75,6 +75,7 @@ - Bidirectional highlighting uses two independent state vars in DashboardLayout: highlightedNodeId (timeline→graph) and highlightedRoleId (graph→timeline) - callbacksRef pattern in CareerConstellation prevents stale closures — always add new callbacks there - LastConsultationSubsection is defined inline in DashboardLayout.tsx, not a separate file +- Link lines are `` elements (not ``) using quadratic bezier curves — tick handler sets `d` attr, not x1/y1/x2/y2. CSS transitions handle highlight animations on stroke properties --- ## 2026-02-16 - US-005 @@ -109,3 +110,17 @@ - LastConsultationSubsection is defined inline in DashboardLayout.tsx, not as a separate file — props must be threaded through the local function definition - D3 mouseenter events on SVG `` elements require direct mouse interaction with the SVG, not the React button overlay layer --- + +## 2026-02-16 - US-007 +- Replaced straight `` elements with curved `` elements for link lines between roles and skills +- Link paths use quadratic bezier curves: `M sx,sy Q cx,sy tx,ty` where cx is the horizontal midpoint — creating a gentle arc that exits horizontally from the role node before curving to the skill +- Added `fill: none` to paths (required since paths auto-fill unlike lines) +- Added CSS transitions on stroke/stroke-opacity/stroke-width (150ms ease) for smooth highlight animations, respecting prefers-reduced-motion +- applyGraphHighlight link styling unchanged — stroke/stroke-opacity/stroke-width attributes work identically on `` as on `` +- Files changed: src/components/CareerConstellation.tsx, Ralph/prd.json +- **Learnings for future iterations:** + - When converting `` to ``, always add `fill: none` — SVG paths default to `fill: black` which would cover the curve area + - Quadratic bezier with control point at `((sx+tx)/2, sy)` creates a nice horizontal-exit curve from role nodes — the path leaves horizontally then arcs down/up to the skill + - CSS transitions work on SVG `` stroke properties, so no D3 `.transition()` needed for link highlight animations (unlike `r` attribute which requires D3 transitions) + - The tick handler generates the `d` attribute string directly — simpler than using `d3.line().curve()` since we only need two-point curves +--- diff --git a/src/components/CareerConstellation.tsx b/src/components/CareerConstellation.tsx index 6092998..2992928 100644 --- a/src/components/CareerConstellation.tsx +++ b/src/components/CareerConstellation.tsx @@ -313,12 +313,17 @@ const CareerConstellation: React.FC = ({ const connectorGroup = svg.append('g').attr('class', 'connectors') const nodeGroup = svg.append('g').attr('class', 'nodes') - const linkSelection = linkGroup.selectAll('line') + const linkSelection = linkGroup.selectAll('path') .data(links) - .join('line') + .join('path') + .attr('fill', 'none') .attr('stroke', 'var(--border-light)') .attr('stroke-width', 1) .attr('stroke-opacity', 0.08) + .style('transition', prefersReducedMotion + ? 'none' + : 'stroke 150ms ease, stroke-opacity 150ms ease, stroke-width 150ms ease' + ) const nodeSelection = nodeGroup.selectAll('g') .data(nodes) @@ -573,10 +578,14 @@ const CareerConstellation: React.FC = ({ }) linkSelection - .attr('x1', d => (d.source as SimNode).x) - .attr('y1', d => (d.source as SimNode).y) - .attr('x2', d => (d.target as SimNode).x) - .attr('y2', d => (d.target as SimNode).y) + .attr('d', d => { + const sx = (d.source as SimNode).x + const sy = (d.source as SimNode).y + const tx = (d.target as SimNode).x + const ty = (d.target as SimNode).y + const cx = (sx + tx) / 2 + return `M${sx},${sy} Q${cx},${sy} ${tx},${ty}` + }) nodeSelection.attr('transform', d => `translate(${d.x},${d.y})`)