ریلِ عکسهای همپوشان
پیشنمایش تمامصفحهبخشِ گالری: کارتهای عکس روی هم مینشینند و کارتِ فعال با هاور، فوکوس یا لمس باز میشود تا عکس و اسمِ داستان دیده شود؛ خودکار جلو میرود و در موبایل ریلِ افقی است.
۱. نصب کامپوننتها
این بلاک از ۴ آیتم دیگ استفاده میکند: buttoncontrol-sizeiconsreveal
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/reveal.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/reveal.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/reveal.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/reveal.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
gallery-reel.tsx
"use client"
import * as React from "react"
import { ArrowUpIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Reveal } from "@/components/ui/reveal"
import { cn } from "@/lib/utils"
import { stockImages, type StockImageName } from "@/lib/stock-images"
const STORIES: { photo: StockImageName; title: string; place: string }[] = [
{ photo: "studioSunlit", title: "کارگاهِ سفالِ نور", place: "اصفهان" },
{ photo: "personWoman1", title: "بوتیکِ لباسِ تابستانی", place: "شیراز" },
{ photo: "personWoman2", title: "کافهکتابِ سرو", place: "تهران" },
{ photo: "foodDish", title: "آشپزخانهٔ خانگیِ مهتاب", place: "رشت" },
{ photo: "personMan1", title: "استودیوی عکاسیِ آینه", place: "تبریز" },
{ photo: "productVase", title: "گالریِ دکورِ کاشی", place: "یزد" },
{ photo: "restaurant", title: "رستورانِ حیاط", place: "کاشان" },
]
/**
* ریلِ عکسهای همپوشان: کارتها روی هم مینشینند و کارتِ فعال باز میشود تا عکس و
* اسمِ داستان کامل دیده شود. با هاور، فوکوسِ کیبورد یا لمس عوض میشود و هر چند ثانیه
* خودش جلو میرود؛ در موبایل ریلِ افقیِ قابلِ کشیدن است.
*/
export default function GalleryReelBlock() {
const [active, setActive] = React.useState(2)
const [paused, setPaused] = React.useState(false)
React.useEffect(() => {
if (paused || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return
const timer = window.setInterval(() => setActive((current) => (current + 1) % STORIES.length), 3200)
return () => window.clearInterval(timer)
}, [paused])
return (
<ControlSizeProvider size="xl" asChild>
<section aria-labelledby="reel-title" className="flex w-full flex-col gap-12 overflow-hidden py-24">
<div className="mx-auto flex w-full max-w-6xl flex-col items-start justify-between gap-6 px-4 sm:px-6 md:flex-row md:items-end">
<div className="flex max-w-xl flex-col gap-3">
<span className="text-sm font-semibold text-brand-secondary">ساختهشده با پیشخوان</span>
<h2 id="reel-title" className="text-3xl leading-snug font-black text-balance sm:text-4xl">
کسبوکارهای کوچک، داستانهای بزرگ
</h2>
<p className="leading-7 text-muted-foreground">بیش از سه هزار فروشگاهِ مستقل با پیشخوان فروش آنلاین را شروع کردهاند.</p>
</div>
<Button variant="outline">
همهٔ داستانها
<ArrowUpIcon aria-hidden className="-rotate-45" />
</Button>
</div>
<Reveal
className="mx-auto flex w-full max-w-6xl snap-x snap-mandatory gap-3 overflow-x-auto px-4 pb-2 [scrollbar-width:none] sm:px-6 md:gap-0 md:overflow-visible md:pb-0 [&::-webkit-scrollbar]:hidden"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
onFocus={() => setPaused(true)}
onBlur={() => setPaused(false)}
>
{STORIES.map((story, index) => {
const open = index === active
return (
<a
key={story.title}
href="#"
aria-current={open ? "true" : undefined}
aria-label={`داستانِ ${story.title}، ${story.place}`}
onMouseEnter={() => setActive(index)}
onFocus={() => setActive(index)}
style={{ zIndex: open ? STORIES.length + 1 : STORIES.length - Math.abs(index - active) }}
className={cn(
"group relative h-96 shrink-0 snap-center overflow-hidden rounded-xl bg-muted text-start shadow-lg outline-none ring-4 ring-background transition-[flex-grow,flex-basis,transform,width] duration-500 ease-out focus-visible:ring-ring",
"w-64 md:w-auto md:basis-0",
index > 0 && "md:-ms-10",
open ? "md:grow-[3.2]" : "md:grow md:scale-[0.94]"
)}
>
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages[story.photo].src} alt="" className="absolute inset-0 size-full object-cover transition-transform duration-700 group-hover:scale-105" />
<span
aria-hidden
className={cn(
"absolute inset-x-0 bottom-0 flex flex-col gap-1 bg-linear-to-t from-[var(--dig-graphite-950)] to-transparent p-5 pt-20 text-[var(--dig-white)] transition-[translate] duration-500",
open ? "translate-y-0" : "md:translate-y-full"
)}
>
<span className="text-lg font-bold">{story.title}</span>
<span className="text-sm text-[var(--dig-neutral-300)]">{story.place}</span>
</span>
</a>
)
})}
</Reveal>
</section>
</ControlSizeProvider>
)
}