diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index a64c215946..f4f16eb73b 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -2,7 +2,7 @@ import type { MotionValue } from 'motion/react' import { useTranslation } from '#i18n' -import { motion, useMotionValue, useSpring, useTransform } from 'motion/react' +import { motion, useMotionValue, useTransform } from 'motion/react' import { useEffect, useLayoutEffect, useRef } from 'react' import marketPlaceBg from '@/public/marketplace/hero-bg.jpg' import marketplaceGradientNoise from '@/public/marketplace/hero-gradient-noise.svg' @@ -36,10 +36,10 @@ export const Description = ({ const heroSubtitleKey = isTemplatesView ? 'marketplace.templatesHeroSubtitle' : 'marketplace.pluginsHeroSubtitle' const rafRef = useRef(null) const lastProgressRef = useRef(0) + const maxScrollableRef = useRef(1) const titleRef = useRef(null) const progress = useMotionValue(0) const titleHeight = useMotionValue(0) - const smoothProgress = useSpring(progress, { stiffness: 260, damping: 34 }) useLayoutEffect(() => { const node = titleRef.current @@ -74,7 +74,17 @@ export const Description = ({ rafRef.current = requestAnimationFrame(() => { const scrollTop = Math.round(container.scrollTop) const heightDelta = container.scrollHeight - container.clientHeight - const effectiveMaxScroll = Math.max(1, Math.min(MAX_SCROLL, heightDelta)) + // Keep collapse threshold stable during the same scroll session. + // If we recompute with shrinking scrollHeight on every frame, progress can stay stuck at 1 + // until scrollTop reaches 0, which feels like "cannot scroll to top". + const observedScrollable = Math.max(1, heightDelta) + const shouldResetThreshold = scrollTop === 0 + if (shouldResetThreshold) + maxScrollableRef.current = observedScrollable + else + maxScrollableRef.current = Math.max(maxScrollableRef.current, observedScrollable) + + const effectiveMaxScroll = Math.max(1, Math.min(MAX_SCROLL, maxScrollableRef.current)) const rawProgress = Math.min(Math.max(scrollTop / effectiveMaxScroll, 0), 1) const snappedProgress = rawProgress >= 0.95 ? 1 @@ -102,16 +112,16 @@ export const Description = ({ }, [progress, scrollContainerId]) // Calculate interpolated values - const contentOpacity = useTransform(smoothProgress, [0, 1], [1, 0]) - const contentScale = useTransform(smoothProgress, [0, 1], [1, 0.9]) + const contentOpacity = useTransform(progress, [0, 1], [1, 0]) + const contentScale = useTransform(progress, [0, 1], [1, 0.9]) const titleMaxHeight: MotionValue = useTransform( - [smoothProgress, titleHeight], + [progress, titleHeight], (values: number[]) => values[1] * (1 - values[0]), ) - const tabsMarginTop = useTransform(smoothProgress, [0, 1], [48, marketplaceNav ? 16 : 0]) - const titleMarginTop = useTransform(smoothProgress, [0, 1], [marketplaceNav ? 80 : 0, 0]) - const paddingTop = useTransform(smoothProgress, [0, 1], [marketplaceNav ? COLLAPSED_PADDING_TOP : EXPANDED_PADDING_TOP, COLLAPSED_PADDING_TOP]) - const paddingBottom = useTransform(smoothProgress, [0, 1], [EXPANDED_PADDING_BOTTOM, COLLAPSED_PADDING_BOTTOM]) + const tabsMarginTop = useTransform(progress, [0, 1], [48, marketplaceNav ? 16 : 0]) + const titleMarginTop = useTransform(progress, [0, 1], [marketplaceNav ? 80 : 0, 0]) + const paddingTop = useTransform(progress, [0, 1], [marketplaceNav ? COLLAPSED_PADDING_TOP : EXPANDED_PADDING_TOP, COLLAPSED_PADDING_TOP]) + const paddingBottom = useTransform(progress, [0, 1], [EXPANDED_PADDING_BOTTOM, COLLAPSED_PADDING_BOTTOM]) return ( {/* Blue base background */}