feat: US-002 - Dynamic height matching with work experience column
This commit is contained in:
@@ -7,11 +7,11 @@ interface CareerConstellationProps {
|
|||||||
onRoleClick: (id: string) => void
|
onRoleClick: (id: string) => void
|
||||||
onSkillClick: (id: string) => void
|
onSkillClick: (id: string) => void
|
||||||
highlightedNodeId?: string | null
|
highlightedNodeId?: string | null
|
||||||
|
containerHeight?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const DESKTOP_HEIGHT = 480
|
const MIN_HEIGHT = 400
|
||||||
const TABLET_HEIGHT = 380
|
const MOBILE_FALLBACK_HEIGHT = 360
|
||||||
const MOBILE_HEIGHT = 310
|
|
||||||
|
|
||||||
const ROLE_RADIUS = 30
|
const ROLE_RADIUS = 30
|
||||||
const SKILL_RADIUS = 14
|
const SKILL_RADIUS = 14
|
||||||
@@ -28,10 +28,12 @@ const domainColorMap: Record<string, string> = {
|
|||||||
const roleNodes = constellationNodes.filter(n => n.type === 'role')
|
const roleNodes = constellationNodes.filter(n => n.type === 'role')
|
||||||
const srDescription = buildScreenReaderDescription()
|
const srDescription = buildScreenReaderDescription()
|
||||||
|
|
||||||
function getHeight(width: number): number {
|
function getHeight(width: number, containerHeight?: number | null): number {
|
||||||
if (width < 768) return MOBILE_HEIGHT
|
// Mobile/tablet: use fallback since columns stack vertically
|
||||||
if (width < 1024) return TABLET_HEIGHT
|
if (width < 1024) return MOBILE_FALLBACK_HEIGHT
|
||||||
return DESKTOP_HEIGHT
|
// Desktop: use measured container height if available, with minimum
|
||||||
|
if (containerHeight && containerHeight > 0) return Math.max(MIN_HEIGHT, containerHeight)
|
||||||
|
return MIN_HEIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SimNode extends ConstellationNode {
|
interface SimNode extends ConstellationNode {
|
||||||
@@ -86,13 +88,14 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
|
|||||||
onRoleClick,
|
onRoleClick,
|
||||||
onSkillClick,
|
onSkillClick,
|
||||||
highlightedNodeId,
|
highlightedNodeId,
|
||||||
|
containerHeight,
|
||||||
}) => {
|
}) => {
|
||||||
const svgRef = useRef<SVGSVGElement>(null)
|
const svgRef = useRef<SVGSVGElement>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const simulationRef = useRef<d3.Simulation<SimNode, SimLink> | null>(null)
|
const simulationRef = useRef<d3.Simulation<SimNode, SimLink> | null>(null)
|
||||||
const highlightGraphRef = useRef<((activeNodeId: string | null) => void) | null>(null)
|
const highlightGraphRef = useRef<((activeNodeId: string | null) => void) | null>(null)
|
||||||
const callbacksRef = useRef({ onRoleClick, onSkillClick })
|
const callbacksRef = useRef({ onRoleClick, onSkillClick })
|
||||||
const [dimensions, setDimensions] = useState({ width: 800, height: DESKTOP_HEIGHT })
|
const [dimensions, setDimensions] = useState({ width: 800, height: MIN_HEIGHT })
|
||||||
const [focusedNodeId, setFocusedNodeId] = useState<string | null>(null)
|
const [focusedNodeId, setFocusedNodeId] = useState<string | null>(null)
|
||||||
const [pinnedNodeId, setPinnedNodeId] = useState<string | null>(null)
|
const [pinnedNodeId, setPinnedNodeId] = useState<string | null>(null)
|
||||||
const [nodeButtonPositions, setNodeButtonPositions] = useState<Record<string, { x: number; y: number }>>({})
|
const [nodeButtonPositions, setNodeButtonPositions] = useState<Record<string, { x: number; y: number }>>({})
|
||||||
@@ -117,7 +120,9 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
|
|||||||
|
|
||||||
const updateDimensions = () => {
|
const updateDimensions = () => {
|
||||||
const width = container.clientWidth
|
const width = container.clientWidth
|
||||||
const height = getHeight(width)
|
// Use viewport width for breakpoint check since container may overflow on mobile
|
||||||
|
const viewportWidth = window.innerWidth
|
||||||
|
const height = getHeight(viewportWidth, containerHeight)
|
||||||
setDimensions({ width, height })
|
setDimensions({ width, height })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +132,7 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
|
|||||||
observer.observe(container)
|
observer.observe(container)
|
||||||
|
|
||||||
return () => observer.disconnect()
|
return () => observer.disconnect()
|
||||||
}, [])
|
}, [containerHeight])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const svg = d3.select(svgRef.current)
|
const svg = d3.select(svgRef.current)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { ChevronRight } from 'lucide-react'
|
import { ChevronRight } from 'lucide-react'
|
||||||
import { TopBar } from './TopBar'
|
import { TopBar } from './TopBar'
|
||||||
@@ -236,9 +236,25 @@ function LastConsultationSubsection() {
|
|||||||
export function DashboardLayout() {
|
export function DashboardLayout() {
|
||||||
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
|
||||||
const [highlightedNodeId, setHighlightedNodeId] = useState<string | null>(null)
|
const [highlightedNodeId, setHighlightedNodeId] = useState<string | null>(null)
|
||||||
|
const [chronologyHeight, setChronologyHeight] = useState<number | null>(null)
|
||||||
|
const chronologyRef = useRef<HTMLDivElement>(null)
|
||||||
const activeSection = useActiveSection()
|
const activeSection = useActiveSection()
|
||||||
const { openPanel } = useDetailPanel()
|
const { openPanel } = useDetailPanel()
|
||||||
|
|
||||||
|
// Measure the chronology stream height so the constellation graph can match it
|
||||||
|
useEffect(() => {
|
||||||
|
const el = chronologyRef.current
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
setChronologyHeight(entry.contentRect.height)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
observer.observe(el)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSearchClick = () => {
|
const handleSearchClick = () => {
|
||||||
setCommandPaletteOpen(true)
|
setCommandPaletteOpen(true)
|
||||||
}
|
}
|
||||||
@@ -383,15 +399,7 @@ export function DashboardLayout() {
|
|||||||
{/* Patient Pathway — parent section with constellation graph + subsections */}
|
{/* Patient Pathway — parent section with constellation graph + subsections */}
|
||||||
<ParentSection title="Patient Pathway" tileId="patient-pathway">
|
<ParentSection title="Patient Pathway" tileId="patient-pathway">
|
||||||
<div className="pathway-columns">
|
<div className="pathway-columns">
|
||||||
<div className="pathway-graph-sticky">
|
<div ref={chronologyRef} className="chronology-stream" data-tile-id="section-experience">
|
||||||
<CareerConstellation
|
|
||||||
onRoleClick={handleRoleClick}
|
|
||||||
onSkillClick={handleSkillClick}
|
|
||||||
highlightedNodeId={highlightedNodeId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chronology-stream" data-tile-id="section-experience">
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '14px',
|
marginBottom: '14px',
|
||||||
@@ -433,6 +441,16 @@ export function DashboardLayout() {
|
|||||||
<EducationSubsection />
|
<EducationSubsection />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="pathway-graph-sticky">
|
||||||
|
<CareerConstellation
|
||||||
|
onRoleClick={handleRoleClick}
|
||||||
|
onSkillClick={handleSkillClick}
|
||||||
|
highlightedNodeId={highlightedNodeId}
|
||||||
|
containerHeight={chronologyHeight}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-tile-id="section-skills" style={{ marginTop: '22px' }}>
|
<div data-tile-id="section-skills" style={{ marginTop: '22px' }}>
|
||||||
|
|||||||
+2
-1
@@ -401,7 +401,7 @@ html {
|
|||||||
/* Desktop: 2 columns */
|
/* Desktop: 2 columns */
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.pathway-columns {
|
.pathway-columns {
|
||||||
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1.5fr);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
gap: 22px;
|
gap: 22px;
|
||||||
}
|
}
|
||||||
@@ -409,6 +409,7 @@ html {
|
|||||||
.pathway-graph-sticky {
|
.pathway-graph-sticky {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user