Task 4b: Fix PatientBanner scroll condensation

Root cause: sentinel element with `absolute top-0` inside PatientBanner was
positioned at viewport top, always triggering the IntersectionObserver's
-100px rootMargin threshold — banner was permanently stuck in condensed state.

Fix: Restructured PMRInterface layout from document-scroll to flex container
with explicit scroll container (`overflow-y-auto` on main). Lifted scroll
condensation logic to PMRInterface, passing `isCondensed` prop down to
PatientBanner. Replaced IntersectionObserver with scroll event listener on
the main element for reliable scroll position detection.

Key changes:
- PMRInterface: flex h-screen overflow-hidden layout (sidebar + content column)
- PatientBanner: accepts isCondensed prop, removed sticky/sentinel/hook
- ClinicalSidebar: h-full instead of h-screen sticky (parent handles sizing)
- useScrollCondensation: scroll event on container element via callback ref

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 00:30:23 +00:00
parent b7471c5cf8
commit d16656b954
4 changed files with 91 additions and 104 deletions
+40 -58
View File
@@ -2,79 +2,61 @@ import { Download, Mail, Linkedin, MoreHorizontal } from 'lucide-react'
import { useState, useRef, useEffect, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { patient } from '@/data/patient'
import { useScrollCondensation } from '@/hooks/useScrollCondensation'
interface PatientBannerProps {
isMobile?: boolean
isTablet?: boolean
isCondensed?: boolean
}
export function PatientBanner({ isMobile = false, isTablet = false }: PatientBannerProps) {
const { isCondensed, sentinelRef } = useScrollCondensation({ threshold: 100 })
export function PatientBanner({ isMobile = false, isTablet = false, isCondensed = false }: PatientBannerProps) {
const prefersReducedMotion = typeof window !== 'undefined'
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
: false
if (isMobile) {
return (
<>
<div
ref={sentinelRef}
className="h-0 w-full absolute top-0"
aria-hidden="true"
/>
<MobileBanner />
</>
)
return <MobileBanner />
}
const shouldCondense = isTablet || isCondensed
return (
<>
<div
ref={sentinelRef}
className="h-0 w-full absolute top-0"
aria-hidden="true"
/>
<header
className={`
sticky top-0 z-40 w-full
bg-pmr-banner border-b border-slate-600
shadow-pmr-banner
transition-all duration-200 ease-out
${shouldCondense ? 'h-12' : 'h-20'}
`}
role="banner"
>
<AnimatePresence mode="wait" initial={false}>
{shouldCondense ? (
<motion.div
key="condensed"
initial={prefersReducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={prefersReducedMotion ? undefined : { opacity: 0 }}
transition={{ duration: 0.15 }}
className="h-full"
>
<CondensedBanner />
</motion.div>
) : (
<motion.div
key="full"
initial={prefersReducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={prefersReducedMotion ? undefined : { opacity: 0 }}
transition={{ duration: 0.15 }}
className="h-full"
>
<FullBanner />
</motion.div>
)}
</AnimatePresence>
</header>
</>
<header
className={`
w-full z-40
bg-pmr-banner border-b border-slate-600
shadow-pmr-banner
transition-all duration-200 ease-out
${shouldCondense ? 'h-12' : 'h-20'}
`}
role="banner"
>
<AnimatePresence mode="wait" initial={false}>
{shouldCondense ? (
<motion.div
key="condensed"
initial={prefersReducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={prefersReducedMotion ? undefined : { opacity: 0 }}
transition={{ duration: 0.15 }}
className="h-full"
>
<CondensedBanner />
</motion.div>
) : (
<motion.div
key="full"
initial={prefersReducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={prefersReducedMotion ? undefined : { opacity: 0 }}
transition={{ duration: 0.15 }}
className="h-full"
>
<FullBanner />
</motion.div>
)}
</AnimatePresence>
</header>
)
}
@@ -97,7 +79,7 @@ function MobileBanner() {
return (
<header
className="sticky top-0 z-40 w-full h-12 bg-pmr-banner border-b border-slate-600 shadow-pmr-banner"
className="w-full z-40 h-12 bg-pmr-banner border-b border-slate-600 shadow-pmr-banner"
role="banner"
>
<div className="h-full px-3 flex items-center justify-between gap-2">