بلاک‌ها · بخش‌های صفحه

گالریِ قاب و نوار

پیش‌نمایش تمام‌صفحه

بخشِ گالری: قابِ بزرگ با تیتر و توضیح و نوارِ تصاویرِ کوچک زیرش؛ انتخابِ هر تصویر قاب را عوض می‌کند و نوارِ زمان خودکار به بعدی می‌رود.

۱. نصب کامپوننت‌ها

این بلاک از ۳ آیتم دیگ استفاده می‌کند: chipcontrol-sizereveal

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/reveal.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/reveal.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/reveal.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/chip.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/reveal.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

gallery-filmstrip.tsx
"use client";

import * as React from "react";

import { Chip } from "@/components/ui/chip";
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 FRAMES: { photo: StockImageName; title: string; text: string }[] = [
  {
    photo: "projectDashboard",
    title: "داشبوردِ فروش",
    text: "گزارشِ روزانه در یک نگاه، بدونِ خروجیِ اکسل.",
  },
  {
    photo: "projectMobileApp",
    title: "اپِ موبایل",
    text: "سفارش‌ها و پیام‌ها را در راه هم جواب بدهید.",
  },
  {
    photo: "projectCafeBrand",
    title: "هویتِ بصری",
    text: "منو، بسته‌بندی و تابلو با یک زبانِ بصری.",
  },
  {
    photo: "coverWorkspace",
    title: "کارِ تیمی",
    text: "هر کس می‌داند امروز نوبتِ کدام کار است.",
  },
];

/**
 * گالریِ قاب و نوار: عکسِ بزرگ با تیتر و توضیحِ روی آن، و زیرش نوارِ تصاویرِ کوچک که
 * با انتخاب، قابِ بزرگ عوض می‌شود؛ نوارِ زمان روی تصویرِ کوچکِ فعال پر می‌شود و
 * خودکار به بعدی می‌رود.
 */
export default function GalleryFilmstripBlock() {
  const [active, setActive] = React.useState(0);
  const [paused, setPaused] = React.useState(false);
  const frame = FRAMES[active];

  React.useEffect(() => {
    if (
      paused ||
      window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
    )
      return;
    const timer = window.setTimeout(
      () => setActive((current) => (current + 1) % FRAMES.length),
      5000,
    );
    return () => window.clearTimeout(timer);
  }, [active, paused]);

  return (
    <ControlSizeProvider size="xl" asChild>
      <section
        aria-labelledby="fs-title"
        className="mx-auto flex w-full max-w-6xl flex-col gap-10 px-4 py-24 sm:px-6"
      >
        <div className="flex max-w-2xl flex-col gap-3">
          <span className="text-sm font-semibold text-brand-secondary">
            محصول
          </span>
          <h2
            id="fs-title"
            className="text-3xl leading-snug font-black text-balance sm:text-4xl"
          >
            همه‌چیزِ کسب‌وکار، در چهار قاب
          </h2>
        </div>
        <Reveal
          className="flex flex-col gap-4"
          onMouseEnter={() => setPaused(true)}
          onMouseLeave={() => setPaused(false)}
        >
          <div className="relative aspect-[4/5] overflow-hidden rounded-xl bg-muted sm:aspect-[16/8]">
            {FRAMES.map((item, index) => (
              // eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ
              <img
                key={item.photo}
                src={stockImages[item.photo].src}
                alt={index === active ? stockImages[item.photo].alt : ""}
                aria-hidden={index !== active}
                className={cn(
                  "absolute inset-0 size-full object-cover transition-[opacity,scale] duration-700",
                  index === active ? "scale-100" : "scale-105 [opacity:0]",
                )}
              />
            ))}
            <div
              aria-live="polite"
              className="absolute inset-x-0 bottom-0 flex flex-col gap-2 bg-linear-to-t from-[var(--dig-graphite-950)] to-transparent p-6 pt-24 text-[var(--dig-white)] sm:p-10 sm:pt-32"
            >
              <h3 className="text-2xl font-black sm:text-3xl">{frame.title}</h3>
              <p className="max-w-md leading-7 text-[var(--dig-neutral-300)]">
                {frame.text}
              </p>
            </div>
          </div>
          <div
            role="tablist"
            aria-label="قاب‌ها"
            className="grid grid-cols-4 gap-2 sm:gap-4"
          >
            {FRAMES.map((item, index) => (
              <Chip asChild key={item.photo} variant="secondary">
                <button
                  type="button"
                  role="tab"
                  aria-selected={index === active}
                  aria-label={item.title}
                  onClick={() => setActive(index)}
                  className={cn(
                    "group relative block h-auto w-full overflow-hidden rounded-lg border-0 bg-transparent p-0 text-start text-base whitespace-normal outline-none ring-offset-2 ring-offset-background focus-visible:ring-2 focus-visible:ring-ring",
                    index === active ? "ring-2 ring-foreground" : "",
                  )}
                >
                  {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                  <img
                    src={stockImages[item.photo].src}
                    alt=""
                    className={cn(
                      "aspect-[16/10] w-full object-cover transition-[filter]",
                      index !== active && "grayscale group-hover:grayscale-0",
                    )}
                  />
                  <span
                    aria-hidden
                    className="absolute inset-x-0 bottom-0 h-1 bg-[color-mix(in_oklab,var(--dig-white)_40%,transparent)]"
                  >
                    {index === active && (
                      <span
                        key={`${active}-${paused}`}
                        className={cn(
                          "block h-full origin-right bg-[var(--dig-white)]",
                          !paused && "animate-[fs-fill_5s_linear_forwards]",
                        )}
                      />
                    )}
                  </span>
                  <span className="hidden p-2 text-start text-sm font-semibold sm:block">
                    {item.title}
                  </span>
                </button>
              </Chip>
            ))}
          </div>
        </Reveal>
        <style>
          {
            "@keyframes fs-fill{from{transform:scaleX(0)}to{transform:scaleX(1)}}"
          }
        </style>
      </section>
    </ControlSizeProvider>
  );
}