عارض المحتوى (Content Carousel)

عارض للمحتوى: سهما السابق والتالي حول بطاقة واحدة أو بطاقتين أو ثلاث بطاقات في العرض الواحد.

التثبيت

bash
npx @smicolon/fasla-ui@latest add content-carousel

قبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.

معاينة

كل بطاقة موضع يقبل أي محتوى. يركب المكوّن محرك scroll-snap نفسه الذي يشغّل عارض الشرائح، فتتحرك البطاقات بالسحب ولوحة التتبّع أيضًا.

دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.
سلمى، قائدة فريق الواجهات
أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.
عمر، مهندس منتج
حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.
لينا، مهندسة تصميم
يتطابق Storybook مع التوثيق ومع قيم التصميم، وهذا أندر مما ينبغي.
كريم، مهندس أول

الأنماط (variant)

محور Type في Figma هو itemsPerView: تتقاسم البطاقات الصف بالتساوي، وتتنازل كل بطاقة عن نصيبها من الفجوات البالغة 16px، ويقدّم السهمان بطاقة واحدة في كل نقرة.

itemsPerView={1}
دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.
سلمى، قائدة فريق الواجهات
أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.
عمر، مهندس منتج
حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.
لينا، مهندسة تصميم
يتطابق Storybook مع التوثيق ومع قيم التصميم، وهذا أندر مما ينبغي.
كريم، مهندس أول
itemsPerView={2}
دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.
سلمى، قائدة فريق الواجهات
أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.
عمر، مهندس منتج
حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.
لينا، مهندسة تصميم
يتطابق Storybook مع التوثيق ومع قيم التصميم، وهذا أندر مما ينبغي.
كريم، مهندس أول
itemsPerView={3}
دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.
سلمى، قائدة فريق الواجهات
أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.
عمر، مهندس منتج
حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.
لينا، مهندسة تصميم
يتطابق Storybook مع التوثيق ومع قيم التصميم، وهذا أندر مما ينبغي.
كريم، مهندس أول

الحالات

يتعطّل السهمان عند الطرفين ويخفتان إلى نصف الشفافية: سهم الرجوع عند أول بطاقة، وسهم التقدّم عند آخر صفحة. وعندما يكون كل المحتوى ظاهرًا يتعطّل الاثنان معًا.

أول بطاقة
دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.
سلمى، قائدة فريق الواجهات
أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.
عمر، مهندس منتج
حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.
لينا، مهندسة تصميم
آخر صفحة
دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.
سلمى، قائدة فريق الواجهات
أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.
عمر، مهندس منتج
حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.
لينا، مهندسة تصميم
كل المحتوى ظاهر
دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.
سلمى، قائدة فريق الواجهات
أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.
عمر، مهندس منتج
حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.
لينا، مهندسة تصميم

إتاحة الوصول (accessibility)

  • الجذر منطقة region مع aria-roledescription بقيمة carousel؛ مرّر دائمًا aria-label لتسميته.
  • السهمان زرّان حقيقيان: يُسمّى سهم الرجوع عبر previousLabel وسهم التقدّم عبر nextLabel، فتصل إليهما لوحة المفاتيح ويسمع قارئ الشاشة لغتك، ويتعطّلان عند الطرفين.
  • مفاتيح الأسهم تنقل بطاقة واحدة من أي موضع في المكوّن، من السهمين أو البطاقات أو المسار، وفق اتجاه النص. وينتقل بمفتاح Home إلى أول بطاقة وبمفتاح End إلى آخرها.
  • السهمان زرّان من مكوّن زر، فيرسم التركيز من لوحة المفاتيح حلقة الزر بعرض 2px، على بعد 2px من السهم. ويرسم المسار الحلقة نفسها عند التركيز عليه.
  • كل بطاقة مجموعة group تُسمّى بالأرقام "2 / 4"؛ مرّر aria-label على البطاقة لتسميتها بمحتواها.
  • التمرير السلس يأتي من CSS وحدها، وتجعل prefers-reduced-motion كل حركة فورية دون أي JavaScript.
  • الاتجاه يُورَث من السمة dir وليس خاصية. يتبادل السهمان موضعيهما مع الصف وتنعكس أيقونتاهما من تلقاء نفسها.

الخصائص (props)

ContentCarousel

الخاصيةالنوعالافتراضيالوصف
itemsPerView1 | 2 | 31عدد البطاقات في العرض الواحد، وهو محور Type في Figma.
activeIndexnumberلا يوجدأول بطاقة ظاهرة عند التحكم بالحالة من الخارج.
defaultActiveIndexnumber0البطاقة المعروضة في البداية.
onActiveIndexChange(index) => voidلا يوجدتُستدعى عند تغيّر أول بطاقة ظاهرة.
previousLabelstring"Previous"الاسم الذي يسمعه قارئ الشاشة لسهم الرجوع.
nextLabelstring"Next"الاسم الذي يسمعه قارئ الشاشة لسهم التقدّم.
aria-labelstringلا يوجديسمّي المكوّن للتقنيات المساعدة. مرّره دائمًا، أو مرّر aria-labelledby بدلًا منه.
childrenReactNodeلا يوجدالبطاقات، لكل بطاقة ContentCarouselItem.
classNamestringلا يوجدأصناف إضافية للجزء.

ContentCarouselItem

الخاصيةالنوعالافتراضيالوصف
aria-labelstringلا يوجديسمّي البطاقة بمحتواها. والاسم الافتراضي موضعها، مثل "2 / 4".
childrenReactNodeلا يوجدمحتوى البطاقة: نص بحشو داخلي خاص به، أو وسائط من حافة إلى حافة.
classNamestringلا يوجدأصناف إضافية للجزء.

الاستخدام

بطاقات مع سهمين

tsx
import {
  ContentCarousel,
  ContentCarouselItem,
} from "@/components/ui/content-carousel"

export function CustomerStories() {
  return (
    <ContentCarousel
      aria-label="قصص العملاء"
      previousLabel="السابق"
      nextLabel="التالي"
    >
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.</blockquote>
      </ContentCarouselItem>
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.</blockquote>
      </ContentCarouselItem>
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.</blockquote>
      </ContentCarouselItem>
    </ContentCarousel>
  )
}

ثلاث بطاقات في العرض الواحد

tsx
import {
  ContentCarousel,
  ContentCarouselItem,
} from "@/components/ui/content-carousel"

export function CustomerStories() {
  return (
    <ContentCarousel
      aria-label="قصص العملاء"
      previousLabel="السابق"
      nextLabel="التالي"
      itemsPerView={3}
    >
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.</blockquote>
      </ContentCarouselItem>
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.</blockquote>
      </ContentCarouselItem>
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.</blockquote>
      </ContentCarouselItem>
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">يتطابق Storybook مع التوثيق ومع قيم التصميم، وهذا أندر مما ينبغي.</blockquote>
      </ContentCarouselItem>
    </ContentCarousel>
  )
}

وسائط من حافة إلى حافة: الموضع بلا حشو داخلي

tsx
import {
  ContentCarousel,
  ContentCarouselItem,
} from "@/components/ui/content-carousel"

const PHOTOS = [
  { src: "/photos/old-town.jpg", alt: "البلدة القديمة عند الفجر" },
  { src: "/photos/harbour.jpg", alt: "سوق الميناء" },
  { src: "/photos/corniche.jpg", alt: "الكورنيش عند الغروب" },
]

export function PhotoTour() {
  return (
    <ContentCarousel
      aria-label="دليل المدينة"
      previousLabel="السابق"
      nextLabel="التالي"
      itemsPerView={2}
    >
      {PHOTOS.map((photo) => (
        <ContentCarouselItem key={photo.src}>
          <img src={photo.src} alt={photo.alt} className="h-full w-full object-cover" />
        </ContentCarouselItem>
      ))}
    </ContentCarousel>
  )
}

التحكم بالحالة

tsx
"use client"

import * as React from "react"
import {
  ContentCarousel,
  ContentCarouselItem,
} from "@/components/ui/content-carousel"

export function CustomerStories() {
  const [index, setIndex] = React.useState(0)
  return (
    <ContentCarousel
      aria-label="قصص العملاء"
      previousLabel="السابق"
      nextLabel="التالي"
      itemsPerView={2}
      activeIndex={index}
      onActiveIndexChange={setIndex}
    >
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">دخلت المكوّنات في مشروعنا دون عناء، وبدت الإعدادات الافتراضية مصمّمة بعناية منذ البداية.</blockquote>
      </ContentCarouselItem>
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">أطلقنا النسخة العربية من لوحة التحكم في أسبوع واحد، وعمل تبديل الاتجاه دون أي تدخل.</blockquote>
      </ContentCarouselItem>
      <ContentCarouselItem>
        <blockquote className="p-6 text-sm">حالات الفراغ والخطأ مدروسة سلفًا، وهذا ما تتركه أغلب المكتبات على عاتقك عادةً.</blockquote>
      </ContentCarouselItem>
    </ContentCarousel>
  )
}