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 )}
); 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 (
-
+
{/* Single row (md+): logo | scrollable nav (左對齊,可橫向滑動) | 搜尋 + 語言 */}
diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index efd42a3..31aa5e9 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -464,13 +464,25 @@ export function Home() { viewAllLabel={t("viewAll")} />
-
+
{latestPosts.slice(0, 5).map((post, index) => ( ))}
+ {/* Desktop: 3-column masonry that matches the Figma layout. */} +
+ {latestPosts.map((post, index) => ( + + + + ))} +