عارض المحتوى (Content Carousel)
عارض للمحتوى: سهما السابق والتالي حول بطاقة واحدة أو بطاقتين أو ثلاث بطاقات في العرض الواحد.
التثبيت
bash
npx @smicolon/fasla-ui@latest add content-carouselقبل إضافة أي مكوّن، شغّل الأمر init مرة واحدة في مشروعك. التفاصيل في صفحة التثبيت.
معاينة
كل بطاقة موضع يقبل أي محتوى. يركب المكوّن محرك scroll-snap نفسه الذي يشغّل عارض الشرائح، فتتحرك البطاقات بالسحب ولوحة التتبّع أيضًا.
الأنماط (variant)
محور Type في Figma هو itemsPerView: تتقاسم البطاقات الصف بالتساوي، وتتنازل كل بطاقة عن نصيبها من الفجوات البالغة 16px، ويقدّم السهمان بطاقة واحدة في كل نقرة.
itemsPerView={1}itemsPerView={2}itemsPerView={3}الحالات
يتعطّل السهمان عند الطرفين ويخفتان إلى نصف الشفافية: سهم الرجوع عند أول بطاقة، وسهم التقدّم عند آخر صفحة. وعندما يكون كل المحتوى ظاهرًا يتعطّل الاثنان معًا.
أول بطاقة
آخر صفحة
كل المحتوى ظاهر
إتاحة الوصول (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
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
itemsPerView | 1 | 2 | 3 | 1 | عدد البطاقات في العرض الواحد، وهو محور Type في Figma. |
activeIndex | number | لا يوجد | أول بطاقة ظاهرة عند التحكم بالحالة من الخارج. |
defaultActiveIndex | number | 0 | البطاقة المعروضة في البداية. |
onActiveIndexChange | (index) => void | لا يوجد | تُستدعى عند تغيّر أول بطاقة ظاهرة. |
previousLabel | string | "Previous" | الاسم الذي يسمعه قارئ الشاشة لسهم الرجوع. |
nextLabel | string | "Next" | الاسم الذي يسمعه قارئ الشاشة لسهم التقدّم. |
aria-label | string | لا يوجد | يسمّي المكوّن للتقنيات المساعدة. مرّره دائمًا، أو مرّر aria-labelledby بدلًا منه. |
children | ReactNode | لا يوجد | البطاقات، لكل بطاقة ContentCarouselItem. |
className | string | لا يوجد | أصناف إضافية للجزء. |
ContentCarouselItem
| الخاصية | النوع | الافتراضي | الوصف |
|---|---|---|---|
aria-label | string | لا يوجد | يسمّي البطاقة بمحتواها. والاسم الافتراضي موضعها، مثل "2 / 4". |
children | ReactNode | لا يوجد | محتوى البطاقة: نص بحشو داخلي خاص به، أو وسائط من حافة إلى حافة. |
className | string | لا يوجد | أصناف إضافية للجزء. |
الاستخدام
بطاقات مع سهمين
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>
)
}