Pre UX polish
This commit is contained in:
@@ -46,6 +46,7 @@ export function DashboardLayout() {
|
||||
const isMobileNav = useIsMobileNav()
|
||||
const chronologyRef = useRef<HTMLDivElement>(null)
|
||||
const patientSummaryRef = useRef<HTMLDivElement>(null)
|
||||
const constellationWrapperRef = useRef<HTMLDivElement>(null)
|
||||
const activeSection = useActiveSection()
|
||||
const { openPanel } = useDetailPanel()
|
||||
const careerConsultationsById = useMemo(
|
||||
@@ -94,18 +95,35 @@ export function DashboardLayout() {
|
||||
return related
|
||||
}, [globalFocusId, nodeTypeById, skillToRoles, roleToSkills])
|
||||
|
||||
// Signal constellation animation readiness when patient summary scrolls out of view
|
||||
// Signal constellation animation readiness:
|
||||
// Desktop (>=768): when patient summary scrolls out of view (graph is side-by-side)
|
||||
// Mobile (<768): when the constellation itself scrolls into view (single-column layout)
|
||||
useEffect(() => {
|
||||
const el = patientSummaryRef.current
|
||||
if (!el) return
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry.isIntersecting) setConstellationReady(true)
|
||||
},
|
||||
{ threshold: 0 },
|
||||
)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
const isMobile = window.innerWidth < 768
|
||||
|
||||
if (isMobile) {
|
||||
const el = constellationWrapperRef.current
|
||||
if (!el) return
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) setConstellationReady(true)
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
} else {
|
||||
const el = patientSummaryRef.current
|
||||
if (!el) return
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (!entry.isIntersecting) setConstellationReady(true)
|
||||
},
|
||||
{ threshold: 0 },
|
||||
)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Measure the chronology stream height so the constellation graph can match it
|
||||
@@ -301,7 +319,7 @@ export function DashboardLayout() {
|
||||
<TimelineInterventionsSubsection onNodeHighlight={handleNodeHighlight} highlightedRoleId={highlightedRoleId} focusRelatedIds={focusRelatedIds} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pathway-graph-sticky">
|
||||
<div ref={constellationWrapperRef} className="pathway-graph-sticky">
|
||||
<CareerConstellation
|
||||
onRoleClick={handleRoleClick}
|
||||
onSkillClick={handleSkillClick}
|
||||
|
||||
@@ -21,7 +21,7 @@ function ProjectItem({
|
||||
onClick,
|
||||
}: ProjectItemProps) {
|
||||
const dotColor = PROJECT_STATUS_COLORS[project.status]
|
||||
const isLive = project.status === 'Live'
|
||||
const livePillLabel = project.demoUrl ? 'Live Demo' : project.externalUrl ? 'Live' : null
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
@@ -118,19 +118,44 @@ function ProjectItem({
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: dotColor,
|
||||
flexShrink: 0,
|
||||
marginTop: '4px',
|
||||
animation: isLive ? 'pulse 2s infinite' : undefined,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span style={{ flex: 1, fontWeight: 500 }}>{project.name}</span>
|
||||
{!livePillLabel && (
|
||||
<div
|
||||
style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: dotColor,
|
||||
flexShrink: 0,
|
||||
marginTop: '4px',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span style={{ flex: 1, fontWeight: 500, display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
|
||||
{project.name}
|
||||
{livePillLabel && (
|
||||
<span
|
||||
className="live-pill"
|
||||
style={{
|
||||
fontSize: '9px',
|
||||
fontFamily: 'var(--font-geist-mono)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
padding: '2px 7px',
|
||||
borderRadius: '9999px',
|
||||
background: 'rgba(34, 197, 94, 0.12)',
|
||||
color: '#16a34a',
|
||||
border: '1px solid rgba(34, 197, 94, 0.3)',
|
||||
animation: 'live-pill-pulse 2s ease-in-out infinite',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: '1.4',
|
||||
}}
|
||||
>
|
||||
{livePillLabel}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
|
||||
@@ -226,6 +226,18 @@ html {
|
||||
}
|
||||
}
|
||||
|
||||
/* Live pill pulse — gentle glow throb */
|
||||
@keyframes live-pill-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 6px 2px rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
/* Login spinner */
|
||||
@keyframes login-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
@@ -681,6 +693,12 @@ textarea:focus-visible {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Static live pill */
|
||||
.live-pill {
|
||||
animation: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Instant smooth scroll override */
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
|
||||
Reference in New Issue
Block a user