terry-staging #14
@@ -6,8 +6,10 @@ import {
|
|||||||
type MouseEvent as ReactMouseEvent,
|
type MouseEvent as ReactMouseEvent,
|
||||||
type PointerEvent as ReactPointerEvent,
|
type PointerEvent as ReactPointerEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { m } from "framer-motion";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api";
|
import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api";
|
||||||
|
import { EASE_OUT } from "../motion";
|
||||||
import { langQuery, useI18n, type Lang } from "../i18n";
|
import { langQuery, useI18n, type Lang } from "../i18n";
|
||||||
|
|
||||||
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
|
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
|
||||||
@@ -123,7 +125,11 @@ export function FigmaBanner() {
|
|||||||
if (!scroller) return;
|
if (!scroller) return;
|
||||||
const target = scroller.children[index] as HTMLElement | undefined;
|
const target = scroller.children[index] as HTMLElement | undefined;
|
||||||
if (!target) return;
|
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(() => {
|
const pauseAutoplay = useCallback(() => {
|
||||||
@@ -168,9 +174,10 @@ export function FigmaBanner() {
|
|||||||
if (!scroller) return;
|
if (!scroller) return;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const width = scroller.clientWidth;
|
const firstChild = scroller.children[0] as HTMLElement | undefined;
|
||||||
if (width === 0) return;
|
const slideWidth = firstChild?.offsetWidth ?? scroller.clientWidth;
|
||||||
const next = Math.round(scroller.scrollLeft / width);
|
if (slideWidth === 0) return;
|
||||||
|
const next = Math.round(scroller.scrollLeft / slideWidth);
|
||||||
setActiveIndex((prev) => (prev === next ? prev : next));
|
setActiveIndex((prev) => (prev === next ? prev : next));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -238,8 +245,9 @@ export function FigmaBanner() {
|
|||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
suppressClickRef.current = false;
|
suppressClickRef.current = false;
|
||||||
}, 0);
|
}, 0);
|
||||||
const width = scroller.clientWidth || 1;
|
const firstChild = scroller.children[0] as HTMLElement | undefined;
|
||||||
const nearest = Math.round(scroller.scrollLeft / width);
|
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));
|
const clamped = Math.max(0, Math.min(slides.length - 1, nearest));
|
||||||
scroller.style.scrollSnapType = "";
|
scroller.style.scrollSnapType = "";
|
||||||
goTo(clamped, "smooth");
|
goTo(clamped, "smooth");
|
||||||
@@ -315,6 +323,7 @@ export function FigmaBanner() {
|
|||||||
aria-label="ARK Library banner"
|
aria-label="ARK Library banner"
|
||||||
>
|
>
|
||||||
{slides.map((slide, index) => {
|
{slides.map((slide, index) => {
|
||||||
|
const isActive = index === activeIndex;
|
||||||
const image = (
|
const image = (
|
||||||
<picture className="block aspect-video w-full overflow-hidden bg-black md:rounded-xl">
|
<picture className="block aspect-video w-full overflow-hidden bg-black md:rounded-xl">
|
||||||
<source media="(max-width: 767px)" srcSet={slide.mobile} />
|
<source media="(max-width: 767px)" srcSet={slide.mobile} />
|
||||||
@@ -330,11 +339,27 @@ export function FigmaBanner() {
|
|||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
);
|
);
|
||||||
|
const animatedImage = (
|
||||||
|
<m.div
|
||||||
|
className="h-full w-full origin-center will-change-transform"
|
||||||
|
animate={{
|
||||||
|
filter: isActive ? "blur(0px)" : "blur(6px)",
|
||||||
|
opacity: isActive ? 1 : 0.55,
|
||||||
|
scale: isActive ? 1 : 0.93,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.45,
|
||||||
|
ease: EASE_OUT as unknown as number[],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{image}
|
||||||
|
</m.div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={slide.id}
|
key={slide.id}
|
||||||
className="relative w-full shrink-0 snap-start"
|
className="relative w-full shrink-0 snap-center md:w-[78%] md:first:ml-[11%] md:last:mr-[11%] lg:w-[72%] lg:first:ml-[14%] lg:last:mr-[14%] xl:w-[60%] xl:first:ml-[20%] xl:last:mr-[20%]"
|
||||||
role="group"
|
role="group"
|
||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
aria-label={`${index + 1} / ${slides.length}`}
|
aria-label={`${index + 1} / ${slides.length}`}
|
||||||
@@ -346,10 +371,10 @@ export function FigmaBanner() {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={(event) => handleSlideClick(event, slide.linkUrl!)}
|
onClick={(event) => handleSlideClick(event, slide.linkUrl!)}
|
||||||
>
|
>
|
||||||
{image}
|
{animatedImage}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
image
|
animatedImage
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,15 @@ export function pickBubble(post: Post): BubbleComponent {
|
|||||||
return FileDocBubble;
|
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 Bubble = pickBubble(post);
|
||||||
const isVisual =
|
const isVisual =
|
||||||
Bubble === AlbumBubble ||
|
Bubble === AlbumBubble ||
|
||||||
@@ -34,7 +42,11 @@ export function MessageBubble({ post }: { post: Post }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`post-${post.id}`}
|
id={`post-${post.id}`}
|
||||||
className="mx-auto w-full max-w-[358px] scroll-mt-[82px] md:max-w-[680px] md:scroll-mt-[98px] lg:max-w-[900px] xl:max-w-[1120px]"
|
className={
|
||||||
|
fluid
|
||||||
|
? "w-full scroll-mt-[82px] md:scroll-mt-[98px]"
|
||||||
|
: "mx-auto w-full max-w-[358px] scroll-mt-[82px] md:max-w-[680px] md:scroll-mt-[98px] lg:max-w-[900px] xl:max-w-[1120px]"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<article
|
<article
|
||||||
className={`relative w-full overflow-hidden rounded-2xl bg-[#272632] text-left shadow-sm ${
|
className={`relative w-full overflow-hidden rounded-2xl bg-[#272632] text-left shadow-sm ${
|
||||||
|
|||||||
@@ -535,7 +535,7 @@ export function PublicLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto hidden max-w-[1280px] px-4 py-[15px] min-[440px]:px-5 sm:px-6 md:block md:px-9 xl:px-0">
|
<div className="mx-auto hidden max-w-[1280px] px-4 py-[15px] min-[440px]:px-5 sm:px-6 md:block md:px-9 xl:px-6">
|
||||||
{/* Single row (md+): logo | scrollable nav (左對齊,可橫向滑動) | 搜尋 + 語言 */}
|
{/* Single row (md+): logo | scrollable nav (左對齊,可橫向滑動) | 搜尋 + 語言 */}
|
||||||
<div className="flex h-10 items-center gap-2 min-[1000px]:gap-0 lg:gap-4">
|
<div className="flex h-10 items-center gap-2 min-[1000px]:gap-0 lg:gap-4">
|
||||||
<div className="flex min-w-0 shrink items-center gap-2.5 text-xl font-bold tracking-wide text-ark-gold">
|
<div className="flex min-w-0 shrink items-center gap-2.5 text-xl font-bold tracking-wide text-ark-gold">
|
||||||
|
|||||||
@@ -464,13 +464,25 @@ export function Home() {
|
|||||||
viewAllLabel={t("viewAll")}
|
viewAllLabel={t("viewAll")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2.5 flex flex-col gap-3 md:mt-7">
|
<div className="mt-2.5 flex flex-col gap-3 md:hidden">
|
||||||
{latestPosts.slice(0, 5).map((post, index) => (
|
{latestPosts.slice(0, 5).map((post, index) => (
|
||||||
<Reveal key={post.id} delay={Math.min(index, 8) * 0.05}>
|
<Reveal key={post.id} delay={Math.min(index, 8) * 0.05}>
|
||||||
<MessageBubble post={post} />
|
<MessageBubble post={post} />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Desktop: 3-column masonry that matches the Figma layout. */}
|
||||||
|
<div className="mt-7 hidden gap-4 md:block md:columns-3">
|
||||||
|
{latestPosts.map((post, index) => (
|
||||||
|
<Reveal
|
||||||
|
key={post.id}
|
||||||
|
delay={Math.min(index, 8) * 0.05}
|
||||||
|
className="mb-4 break-inside-avoid"
|
||||||
|
>
|
||||||
|
<MessageBubble post={post} fluid />
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|||||||
Reference in New Issue
Block a user