From c7e0562d9af0a8b3329d3db1b744da60742b222d Mon Sep 17 00:00:00 2001 From: TerryM Date: Sun, 31 May 2026 18:22:03 +0800 Subject: [PATCH] feat: desktop banner peek with framer-motion blur Match Figma node 4366-11092 desktop banner design: - Slides shrink to 78%/72%/60% width on md/lg/xl with snap-center, first/last get matching left/right margin so the edges still center. - Each slide is wrapped in a framer-motion m.div that animates filter, opacity, and scale between active and idle states. - goTo and scroll/drag handlers use the slide's real offsetWidth so centering math holds at every breakpoint; mobile (full-width, snap-start visual) is unchanged. --- src/components/FigmaBanner.tsx | 43 +++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/components/FigmaBanner.tsx b/src/components/FigmaBanner.tsx index f32e491..bfae6b3 100644 --- a/src/components/FigmaBanner.tsx +++ b/src/components/FigmaBanner.tsx @@ -6,8 +6,10 @@ import { type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, } from "react"; +import { m } from "framer-motion"; import { useNavigate } from "react-router-dom"; import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api"; +import { EASE_OUT } from "../motion"; import { langQuery, useI18n, type Lang } from "../i18n"; const FIGMA_ASSET_BASE = "/assets/ark-library/figma"; @@ -123,7 +125,11 @@ export function FigmaBanner() { if (!scroller) return; const target = scroller.children[index] as HTMLElement | undefined; if (!target) return; - scroller.scrollTo({ left: target.offsetLeft, behavior }); + // Center the slide in the viewport. On mobile (full-width slides) the + // offset is 0 so this matches the previous snap-start behavior. + const centerOffset = (scroller.clientWidth - target.offsetWidth) / 2; + const left = Math.max(0, target.offsetLeft - centerOffset); + scroller.scrollTo({ left, behavior }); }, []); const pauseAutoplay = useCallback(() => { @@ -168,9 +174,10 @@ export function FigmaBanner() { if (!scroller) return; const handleScroll = () => { - const width = scroller.clientWidth; - if (width === 0) return; - const next = Math.round(scroller.scrollLeft / width); + const firstChild = scroller.children[0] as HTMLElement | undefined; + const slideWidth = firstChild?.offsetWidth ?? scroller.clientWidth; + if (slideWidth === 0) return; + const next = Math.round(scroller.scrollLeft / slideWidth); setActiveIndex((prev) => (prev === next ? prev : next)); }; @@ -238,8 +245,9 @@ export function FigmaBanner() { window.setTimeout(() => { suppressClickRef.current = false; }, 0); - const width = scroller.clientWidth || 1; - const nearest = Math.round(scroller.scrollLeft / width); + const firstChild = scroller.children[0] as HTMLElement | undefined; + const slideWidth = firstChild?.offsetWidth || scroller.clientWidth || 1; + const nearest = Math.round(scroller.scrollLeft / slideWidth); const clamped = Math.max(0, Math.min(slides.length - 1, nearest)); scroller.style.scrollSnapType = ""; goTo(clamped, "smooth"); @@ -315,6 +323,7 @@ export function FigmaBanner() { aria-label="ARK Library banner" > {slides.map((slide, index) => { + const isActive = index === activeIndex; const image = ( @@ -330,11 +339,27 @@ export function FigmaBanner() { /> ); + const animatedImage = ( + + {image} + + ); return (
handleSlideClick(event, slide.linkUrl!)} > - {image} + {animatedImage} ) : ( - image + animatedImage )}
);