شريط جانبي (Sidebar)

يعرض مكوّن شريط جانبي تنقّل التطبيق، وينطوي إلى أيقونات: مجموعات من الروابط لكلٍّ منها عنوان، وعناصر فرعية تحت عنصر رئيسي، والمستخدم الذي سجّل دخوله في أسفله. يأتي بحالتَي التوسيع والطيّ كما في Figma، مع بطاقة خطة اختيارية.

التثبيت

bash
npx @smicolon/fasla-ui@latest add sidebar

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

مكوّن الشريط الجانبي مبني على مكتبة Base UI، لذلك يحتاج إلى الحزمة @base-ui/react، ويستخدم المكوّن avatar والمكوّن tooltip. يثبّت الأمر add كليهما مع المكوّن، وكذلك أداة shadcn.

معاينة

اطوِ الشريط الجانبي من الزر في رأسه، ثم ضع المؤشر على أيقونة لترى تسميتها.

التوسيع والطيّ

الشريط الجانبي موسَّعًا بتسمياته وعناوين أقسامه، ومطويًا إلى عمود من الأيقونات.

موسَّع
مطوي

الحالات

الحالات الأربع للعنصر والعنصر الفرعي: الحالة الافتراضية، ومرور المؤشر، والتحديد، والتركيز.

افتراضي
مرور المؤشر
محدَّد
التركيز

العناصر والعناصر الفرعية

عناصر بأيقونة ونقطة إشعار، وعناصر فرعية تُفتح وتُغلق تحت العنصر الرئيسي.

بطاقة الخطة

بطاقة الخطة الاختيارية، وتظهر فوق الملف الشخصي أسفل الشريط الجانبي.

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

  • الشريط الجانبي عنصر aside، والمكوّن SidebarContent عنصر nav. أعطِ العنصر nav الخاصية aria-label إذا كانت في الصفحة منطقة تنقّل أخرى.
  • تعيّن الخاصية selected القيمة aria-current="page" على رابط العنصر، فيعلن قارئ الشاشة أنه الصفحة الحالية.
  • اسم الزر في رأس الشريط «طيّ الشريط الجانبي» أو «توسيع الشريط الجانبي» في الصفحة العربية، و«Collapse sidebar» أو «Expand sidebar» في الإنجليزية، ويعلن حالته بالخاصية aria-expanded. ويمكنك تغيير الاسمين بالخاصية collapseLabel والخاصية expandLabel.
  • العنصر الذي له عناصر فرعية زرٌّ يحمل الخاصية aria-expanded. يفتحه مفتاح Enter ومفتاح المسافة ويغلقانه.
  • حين يكون الشريط مطويًا، تحتفظ كل أيقونة بتسميتها اسمًا لدى قارئ الشاشة. ويظهر التلميح عند مرور المؤشر وعند التركيز بلوحة المفاتيح، ويغلقه مفتاح Escape.
  • يرسم التركيز بلوحة المفاتيح حلقة التركيز نفسها التي في مكوّن حقل الاختيار: حدّ بلون ring مع هالة عرضها 3px بلون ring بشفافية 50%. وفي وضع الألوان القسرية يرسم إطارًا بلون Highlight عرضه 2px بدلًا منها.
  • اسم نقطة الإشعار «جديد» في الصفحة العربية و«New» في الإنجليزية. عيّن الخاصية badgeLabel لتقول أكثر، مثل عدد التحديثات.
  • يتغيّر عرض الشريط وتُفتح العناصر الفرعية وتُغلق بحركة مدتها 180ms. وإذا فعّل المستخدم إعداد تقليل الحركة، تتغيّر فورًا.
  • يأخذ الشريط الجانبي اتجاه الصفحة. ففي الصفحة العربية يقع على اليمين وتنعكس كل المسافات، وتنقلب أيقونة زر الطيّ والأسهم. ولا يحتاج الاتجاه إلى خاصية.

الخصائص (props)

Sidebar

الخاصيةالنوعالافتراضيالوصف
collapsedbooleanلا يوجديطوي الشريط فلا تظهر إلا الأيقونات، عند التحكّم بالقيمة.
defaultCollapsedbooleanfalseيبدأ الشريط مطويًا، حين لا تتحكّم بالقيمة.
onCollapsedChange(collapsed) => voidلا يوجدتُستدعى بالحالة الجديدة عند طيّ الشريط أو توسيعه.
childrenReactNodeلا يوجدالرأس والمحتوى وبطاقة الخطة والتذييل، بهذا الترتيب. وتختفي بطاقة الخطة ما دام الشريط مطويًا.
classNamestringلا يوجدأصناف CSS إضافية للعنصر aside.

SidebarHeader

الخاصيةالنوعالافتراضيالوصف
logoReactNodeلا يوجدالشعار كاملًا، ويظهر والشريط موسَّع. في Figma عرض الشعار الإنجليزي 90px وارتفاع الشعار العربي 28px.
logoIconReactNodeلا يوجدرمز الشعار وحده، ويظهر والشريط مطوي. قياسه في Figma هو 28px.
collapseLabelstring"Collapse sidebar"اسم زر الطيّ لدى قارئ الشاشة والشريط موسَّع.
expandLabelstring"Expand sidebar"اسم زر التوسيع لدى قارئ الشاشة والشريط مطوي.
classNamestringلا يوجدأصناف CSS إضافية للرأس.

SidebarContent

الخاصيةالنوعالافتراضيالوصف
aria-labelstringلا يوجديسمّي منطقة التنقّل.
childrenReactNodeلا يوجدمكوّنات SidebarGroup.
classNamestringلا يوجدأصناف CSS إضافية للعنصر nav.

SidebarGroup

الخاصيةالنوعالافتراضيالوصف
labelReactNodeلا يوجدعنوان القسم. يتحوّل إلى خط فاصل عند طيّ الشريط، ويبقى اسم المجموعة لدى قارئ الشاشة.
childrenReactNodeلا يوجدمكوّنات SidebarItem.
classNamestringلا يوجدأصناف CSS إضافية للمجموعة.

SidebarItem

الخاصيةالنوعالافتراضيالوصف
labelReactNodeلا يوجدنص العنصر. يصبح التلميح واسم العنصر لدى قارئ الشاشة عند طيّ الشريط.
iconReactNodeلا يوجدأيقونة بقياس 16px قبل التسمية. لا تظهر إلا هي عند طيّ الشريط، لذلك أعطِ كل عنصر أيقونة.
selectedbooleanfalseيعلّم العنصر بأنه الصفحة الحالية. عيّنها لعنصر واحد فقط.
badgebooleanfalseيعرض نقطة الإشعار في نهاية الصف.
badgeLabelstring"New"اسم النقطة لدى قارئ الشاشة.
hrefstringلا يوجديجعل العنصر رابطًا إلى هذا العنوان.
renderReactElementلا يوجديعرض هذا العنصر بدل الرابط، مثل المكوّن Link في أداة التوجيه لديك.
openbooleanلا يوجديُظهر العناصر الفرعية أو يخفيها، عند التحكّم بالقيمة.
defaultOpenbooleanلا يوجديُظهر العناصر الفرعية عند العرض الأول. ويكون مفتوحًا افتراضيًا إذا كان أحدها محدَّدًا.
onOpenChange(open) => voidلا يوجدتُستدعى عند فتح العناصر الفرعية أو إغلاقها.
onClick(event) => voidلا يوجدتُستدعى عند الضغط على العنصر.
childrenReactNodeلا يوجدمكوّنات SidebarSubItem، بمستوى واحد فقط. إذا وُجد أيٌّ منها، فتح العنصر عناصره الفرعية وأغلقها بدل أن يكون رابطًا.
classNamestringلا يوجدأصناف CSS إضافية لرابط العنصر أو زره.

SidebarSubItem

الخاصيةالنوعالافتراضيالوصف
labelReactNodeلا يوجدنص العنصر الفرعي.
selectedbooleanfalseيعلّم العنصر بأنه الصفحة الحالية. ويُفتح العنصر الرئيسي ليُظهره، ويظهر محدَّدًا عند طيّ الشريط.
badgebooleanfalseيعرض نقطة الإشعار في نهاية الصف.
badgeLabelstring"New"اسم النقطة لدى قارئ الشاشة.
hrefstringلا يوجديجعل العنصر الفرعي رابطًا إلى هذا العنوان.
renderReactElementلا يوجديعرض هذا العنصر بدل الرابط، مثل المكوّن Link في أداة التوجيه لديك.
onClick(event) => voidلا يوجدتُستدعى عند الضغط على العنصر الفرعي.
classNamestringلا يوجدأصناف CSS إضافية لرابط العنصر الفرعي أو زره.

SidebarPlanCard

الخاصيةالنوعالافتراضيالوصف
titleReactNodeلا يوجداسم الخطة.
descriptionReactNodeلا يوجدسطر تحت الاسم.
footerReactNodeلا يوجدالسطر تحت الخط الفاصل.
footerIconReactNodeلا يوجدأيقونة بقياس 16px قبل footer.
classNamestringلا يوجدأصناف CSS إضافية للبطاقة.

SidebarFooter

الخاصيةالنوعالافتراضيالوصف
childrenReactNodeلا يوجدالمكوّن SidebarProfile.
classNamestringلا يوجدأصناف CSS إضافية للتذييل.

SidebarProfile

الخاصيةالنوعالافتراضيالوصف
namestringلا يوجداسم الشخص. يصبح اسم الزر لدى قارئ الشاشة عند طيّ الشريط.
descriptionReactNodeلا يوجدسطر تحت الاسم، مثل مساحة العمل.
avatarAvatarPropsلا يوجدخصائص الصورة الرمزية، مثل src. تظهر بقياس 32px.
onClick(event) => voidلا يوجدتُستدعى عند الضغط على الملف الشخصي، مثلًا لفتح قائمة الحساب.
classNamestringلا يوجدأصناف CSS إضافية لزر الملف الشخصي.

الاستخدام

شريط جانبي لتطبيق

tsx
import { LayoutDashboardIcon, SettingsIcon, ShoppingCartIcon } from "lucide-react"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarHeader,
  SidebarItem,
  SidebarProfile,
  SidebarSubItem,
} from "@/components/ui/sidebar"

export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarHeader
        logo={<img src="/logo.svg" alt="فاصلة" className="h-7" />}
        logoIcon={<img src="/logo-icon.svg" alt="فاصلة" className="size-7" />}
      />
      <SidebarContent aria-label="الرئيسية">
        <SidebarGroup label="المتجر">
          <SidebarItem icon={<LayoutDashboardIcon strokeWidth={1.5} />} label="لوحة التحكم" href="/dashboard" />
          <SidebarItem icon={<ShoppingCartIcon strokeWidth={1.5} />} label="الطلبات">
            <SidebarSubItem label="كل الطلبات" href="/orders" selected />
            <SidebarSubItem label="المرتجعات" href="/orders/returns" />
          </SidebarItem>
        </SidebarGroup>
        <SidebarGroup label="الحساب">
          <SidebarItem icon={<SettingsIcon strokeWidth={1.5} />} label="الإعدادات" href="/settings" />
        </SidebarGroup>
      </SidebarContent>
      <SidebarFooter>
        <SidebarProfile name="نادية رحمن" description="فاصلة" avatar={{ src: "/avatar.png" }} />
      </SidebarFooter>
    </Sidebar>
  )
}

التحكّم بحالة الطيّ

tsx
"use client"

import { useState } from "react"
import { LayoutDashboardIcon } from "lucide-react"
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarHeader,
  SidebarItem,
} from "@/components/ui/sidebar"

export function CollapsibleSidebar() {
  // Keep it in your layout state, or in a cookie, to remember it between pages.
  const [collapsed, setCollapsed] = useState(false)

  return (
    <Sidebar collapsed={collapsed} onCollapsedChange={setCollapsed}>
      <SidebarHeader logo={<img src="/logo.svg" alt="فاصلة" className="h-7" />} />
      <SidebarContent>
        <SidebarGroup label="المتجر">
          <SidebarItem icon={<LayoutDashboardIcon strokeWidth={1.5} />} label="لوحة التحكم" href="/dashboard" />
        </SidebarGroup>
      </SidebarContent>
    </Sidebar>
  )
}

الربط بأداة التوجيه

tsx
"use client"

import Link from "next/link"
import { usePathname } from "next/navigation"
import { LayoutDashboardIcon, ShoppingCartIcon } from "lucide-react"
import { Sidebar, SidebarContent, SidebarGroup, SidebarItem } from "@/components/ui/sidebar"

const links = [
  { href: "/dashboard", label: "لوحة التحكم", icon: LayoutDashboardIcon },
  { href: "/orders", label: "الطلبات", icon: ShoppingCartIcon },
]

export function RouterSidebar() {
  const pathname = usePathname()

  return (
    <Sidebar>
      <SidebarContent>
        <SidebarGroup label="المتجر">
          {links.map(({ href, label, icon: Icon }) => (
            <SidebarItem
              key={href}
              label={label}
              icon={<Icon strokeWidth={1.5} />}
              render={<Link href={href} />}
              selected={pathname === href}
            />
          ))}
        </SidebarGroup>
      </SidebarContent>
    </Sidebar>
  )
}

إضافة بطاقة الخطة

tsx
import { CalendarIcon, LayoutDashboardIcon } from "lucide-react"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarItem,
  SidebarPlanCard,
  SidebarProfile,
} from "@/components/ui/sidebar"

export function SidebarWithPlan() {
  return (
    <Sidebar>
      <SidebarContent>
        <SidebarGroup label="المتجر">
          <SidebarItem icon={<LayoutDashboardIcon strokeWidth={1.5} />} label="لوحة التحكم" href="/dashboard" />
        </SidebarGroup>
      </SidebarContent>
      <SidebarPlanCard
        title="الخطة الاحترافية"
        description="3 أعضاء في الفريق"
        footer="التجديد التالي: 10 مارس"
        footerIcon={<CalendarIcon strokeWidth={1.5} />}
      />
      <SidebarFooter>
        <SidebarProfile name="نادية رحمن" description="فاصلة" />
      </SidebarFooter>
    </Sidebar>
  )
}