From c7e0562d9af0a8b3329d3db1b744da60742b222d Mon Sep 17 00:00:00 2001 From: TerryM Date: Sun, 31 May 2026 18:22:03 +0800 Subject: [PATCH 1/3] 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 )}
); From 34ef6cba1576d8ce309fb390f1001df3d2718d85 Mon Sep 17 00:00:00 2001 From: TerryM Date: Sun, 31 May 2026 18:35:20 +0800 Subject: [PATCH 2/3] fix: ensure minimum horizontal padding on desktop header at all viewports --- src/layouts/PublicLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index dc2834a..68916df 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -535,7 +535,7 @@ export function PublicLayout() { -
+
{/* Single row (md+): logo | scrollable nav (左對齊,可橫向滑動) | 搜尋 + 語言 */}
From 06fe117ebc8f4121e5875191872c4f6ed50e38ef Mon Sep 17 00:00:00 2001 From: TerryM Date: Sun, 31 May 2026 18:35:57 +0800 Subject: [PATCH 3/3] feat: render desktop latest section as 3-column masonry - Matches Figma design (file uHDZkVHjAp7BXDKQKB0PM4, node 4367-11405). - Mobile keeps the existing 5-post single column unchanged. - Desktop (md+) renders all 12 latest posts in a CSS-columns masonry with break-inside-avoid so each card's height stays content-driven. - Adds an optional 'fluid' prop to MessageBubble that drops the standalone-feed max-widths so bubbles fill the masonry column. The /browse stream keeps the default non-fluid widths. --- src/components/messageStream/MessageBubble.tsx | 16 ++++++++++++++-- src/pages/Home/index.tsx | 14 +++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/messageStream/MessageBubble.tsx b/src/components/messageStream/MessageBubble.tsx index 5312977..d8fe2f9 100644 --- a/src/components/messageStream/MessageBubble.tsx +++ b/src/components/messageStream/MessageBubble.tsx @@ -23,7 +23,15 @@ export function pickBubble(post: Post): BubbleComponent { return FileDocBubble; } -export function MessageBubble({ post }: { post: Post }) { +export function MessageBubble({ + post, + fluid = false, +}: { + post: Post; + /** When true, fill the parent container instead of applying the standalone + * feed max-widths. Used by the desktop 3-column masonry on the home page. */ + fluid?: boolean; +}) { const Bubble = pickBubble(post); const isVisual = Bubble === AlbumBubble || @@ -34,7 +42,11 @@ export function MessageBubble({ post }: { post: Post }) { return (
-
+
{latestPosts.slice(0, 5).map((post, index) => ( ))}
+ {/* Desktop: 3-column masonry that matches the Figma layout. */} +
+ {latestPosts.map((post, index) => ( + + + + ))} +