دليل شامل لـ shadcn/ui مع Next.js: بناء واجهات حديثة

AI Bot
بواسطة AI Bot ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

مقدمة

أصبح shadcn/ui المرجع الأساسي لبناء واجهات حديثة باستخدام React و Next.js. على عكس مكتبات المكونات التقليدية مثل Material UI أو Chakra UI، يتبنى shadcn/ui نهجاً مختلفاً جذرياً: بدلاً من تثبيت حزمة npm، تقوم بنسخ المكونات مباشرة في مشروعك. هذا يمنحك التحكم الكامل في الكود والأنماط وسلوك كل مكون.

في هذا الدليل، ستتعلم كيفية إعداد shadcn/ui في مشروع Next.js، وتخصيص السمة، وبناء مكونات معقدة مثل النماذج وجداول البيانات، وتطبيق أفضل الممارسات لإنشاء تطبيقات بجودة احترافية.

ما ستتعلمه

  • تثبيت وتكوين shadcn/ui في مشروع Next.js
  • استخدام المكونات الأكثر شيوعاً (Button, Card, Dialog, Form)
  • تخصيص السمة باستخدام متغيرات CSS
  • بناء نموذج كامل مع التحقق من الصحة باستخدام React Hook Form و Zod
  • إنشاء جدول بيانات تفاعلي مع الفرز والتصفية
  • تنفيذ الوضع الداكن (Dark Mode)
  • تنظيم مكوناتك بطريقة قابلة للصيانة

المتطلبات المسبقة

قبل البدء، تأكد من وجود:

  • Node.js 18+ مثبت على جهازك
  • معرفة أساسية بـ React و TypeScript
  • إلمام بـ Tailwind CSS
  • محرر أكواد (يُنصح بـ VS Code)

الخطوة 1: إنشاء مشروع Next.js

ابدأ بإنشاء مشروع Next.js جديد مع TypeScript و Tailwind CSS:

npx create-next-app@latest my-shadcn-app --typescript --tailwind --eslint --app --src-dir
cd my-shadcn-app

اختر الخيارات التالية أثناء الإعداد:

  • Would you like to use App Router? Yes
  • Would you like to customize the default import alias? Yes، استخدم @/*

الخطوة 2: تثبيت shadcn/ui

نفّذ أمر تهيئة shadcn/ui:

npx shadcn@latest init

سيسألك معالج الإعداد عدة أسئلة:

Which style would you like to use? › New York
Which color would you like to use as base color? › Neutral
Do you want to use CSS variables for colors? › yes

سيقوم هذا الأمر بـ:

  • إنشاء ملف components.json في جذر المشروع
  • إضافة الأدوات المساعدة في lib/utils.ts
  • تكوين متغيرات CSS في ملف globals.css
  • تحديث tailwind.config.ts بمسارات المكونات

لنفحص ملف components.json المُنشأ:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "src/app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}

الخطوة 3: إضافة مكوناتك الأولى

يوفر shadcn/ui واجهة سطر أوامر لإضافة المكونات بشكل فردي. لنضف المكونات الأساسية:

npx shadcn@latest add button card input label

يتم نسخ كل مكون في src/components/ui/. لنلقِ نظرة على مكون Button:

// src/components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
 
const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
        destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
        outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
        secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3 text-xs",
        lg: "h-10 rounded-md px-8",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)
 
export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}
 
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"
 
export { Button, buttonVariants }

النمط واضح: كل مكون يستخدم cva (Class Variance Authority) لإدارة المتغيرات و cn (غلاف حول clsx و tailwind-merge) لدمج فئات CSS.

الخطوة 4: بناء صفحة بالمكونات

لنعدّل الصفحة الرئيسية لاستخدام مكوناتنا:

// src/app/page.tsx
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
 
export default function Home() {
  return (
    <main className="container mx-auto py-10">
      <h1 className="text-4xl font-bold mb-8">
        تطبيقي مع shadcn/ui
      </h1>
 
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        <Card>
          <CardHeader>
            <CardTitle>مكونات سهلة الوصول</CardTitle>
            <CardDescription>
              مبنية على Radix UI، جميع المكونات سهلة
              الوصول بشكل افتراضي.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm text-muted-foreground">
              التنقل بلوحة المفاتيح وقارئات الشاشة
              وتوافق ARIA مدمجة.
            </p>
          </CardContent>
          <CardFooter>
            <Button>اعرف المزيد</Button>
          </CardFooter>
        </Card>
 
        <Card>
          <CardHeader>
            <CardTitle>قابل للتخصيص</CardTitle>
            <CardDescription>
              الكود موجود في مشروعك. عدّل كل شيء
              حسب احتياجاتك.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm text-muted-foreground">
              متغيرات CSS و Tailwind ومكونات
              قابلة للتعديل بالكامل.
            </p>
          </CardContent>
          <CardFooter>
            <Button variant="outline">استكشف</Button>
          </CardFooter>
        </Card>
 
        <Card>
          <CardHeader>
            <CardTitle>TypeScript أصلي</CardTitle>
            <CardDescription>
              أنواع صارمة وإكمال تلقائي لتجربة
              مطور مثالية.
            </CardDescription>
          </CardHeader>
          <CardContent>
            <p className="text-sm text-muted-foreground">
              كل مكون مُحدد الأنواع بالكامل مع
              خصائص واضحة المعالم.
            </p>
          </CardContent>
          <CardFooter>
            <Button variant="secondary">اكتشف</Button>
          </CardFooter>
        </Card>
      </div>
    </main>
  )
}

شغّل خادم التطوير لرؤية النتيجة:

npm run dev

الخطوة 5: تخصيص السمة

يعتمد نظام السمات في shadcn/ui على متغيرات CSS. افتح src/app/globals.css لرؤية المتغيرات المُنشأة:

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 0 0% 3.9%;
    --card: 0 0% 100%;
    --card-foreground: 0 0% 3.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 0 0% 3.9%;
    --primary: 0 0% 9%;
    --primary-foreground: 0 0% 98%;
    --secondary: 0 0% 96.1%;
    --secondary-foreground: 0 0% 9%;
    --muted: 0 0% 96.1%;
    --muted-foreground: 0 0% 45.1%;
    --accent: 0 0% 96.1%;
    --accent-foreground: 0 0% 9%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
    --border: 0 0% 89.8%;
    --input: 0 0% 89.8%;
    --ring: 0 0% 3.9%;
    --radius: 0.5rem;
  }
 
  .dark {
    --background: 0 0% 3.9%;
    --foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 0 0% 9%;
    /* ... متغيرات الوضع الداكن الأخرى */
  }
}

لإنشاء سمة مخصصة، عدّل هذه المتغيرات. مثلاً، لسمة زرقاء احترافية:

:root {
  --primary: 221.2 83.2% 53.3%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
  --accent: 210 40% 96.1%;
  --accent-foreground: 222.2 47.4% 11.2%;
  --radius: 0.75rem;
}

يمكنك أيضاً استخدام مُنشئ السمات عبر الإنترنت الخاص بـ shadcn/ui لإنشاء لوحة ألوانك بصرياً ونسخ متغيرات CSS المُنشأة.

الخطوة 6: نموذج مع التحقق من الصحة

أحد أكثر حالات الاستخدام شيوعاً هو إنشاء النماذج. يتكامل shadcn/ui بسلاسة مع React Hook Form و Zod للتحقق من الصحة.

ثبّت التبعيات اللازمة:

npx shadcn@latest add form select textarea toast
npm install zod

أنشئ نموذج اتصال كاملاً:

// src/components/contact-form.tsx
"use client"
 
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { useToast } from "@/hooks/use-toast"
 
const formSchema = z.object({
  name: z
    .string()
    .min(2, "يجب أن يحتوي الاسم على حرفين على الأقل"),
  email: z
    .string()
    .email("عنوان بريد إلكتروني غير صالح"),
  subject: z
    .string()
    .min(1, "يرجى اختيار موضوع"),
  message: z
    .string()
    .min(10, "يجب أن تحتوي الرسالة على 10 أحرف على الأقل")
    .max(500, "يجب ألا تتجاوز الرسالة 500 حرف"),
})
 
type FormValues = z.infer<typeof formSchema>
 
export function ContactForm() {
  const { toast } = useToast()
 
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
      email: "",
      subject: "",
      message: "",
    },
  })
 
  function onSubmit(values: FormValues) {
    console.log(values)
    toast({
      title: "تم إرسال الرسالة!",
      description: "سنرد عليك خلال 24 ساعة.",
    })
    form.reset()
  }
 
  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit)}
        className="space-y-6"
      >
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>الاسم الكامل</FormLabel>
              <FormControl>
                <Input placeholder="أحمد محمد" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>البريد الإلكتروني</FormLabel>
              <FormControl>
                <Input
                  type="email"
                  placeholder="ahmed@example.com"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <FormField
          control={form.control}
          name="subject"
          render={({ field }) => (
            <FormItem>
              <FormLabel>الموضوع</FormLabel>
              <Select
                onValueChange={field.onChange}
                defaultValue={field.value}
              >
                <FormControl>
                  <SelectTrigger>
                    <SelectValue placeholder="اختر موضوعاً" />
                  </SelectTrigger>
                </FormControl>
                <SelectContent>
                  <SelectItem value="general">
                    استفسار عام
                  </SelectItem>
                  <SelectItem value="support">
                    دعم فني
                  </SelectItem>
                  <SelectItem value="sales">
                    استفسار تجاري
                  </SelectItem>
                  <SelectItem value="partnership">
                    شراكة
                  </SelectItem>
                </SelectContent>
              </Select>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <FormField
          control={form.control}
          name="message"
          render={({ field }) => (
            <FormItem>
              <FormLabel>الرسالة</FormLabel>
              <FormControl>
                <Textarea
                  placeholder="صف طلبك..."
                  className="min-h-[120px]"
                  {...field}
                />
              </FormControl>
              <FormDescription>
                بين 10 و 500 حرف.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
 
        <Button type="submit" className="w-full">
          إرسال الرسالة
        </Button>
      </form>
    </Form>
  )
}

يوفر هذا النموذج:

  • التحقق من جانب العميل مع رسائل خطأ واضحة بالعربية
  • أنواع TypeScript مُستنتجة تلقائياً من مخطط Zod
  • إمكانية الوصول: تسميات مرتبطة ورسائل خطأ مرتبطة بالحقول
  • ملاحظات المستخدم عبر إشعارات toast

الخطوة 7: جدول بيانات تفاعلي

جداول البيانات هي مكون آخر يُستخدم بشكل متكرر. يوفر shadcn/ui مكون Table يتكامل مع TanStack Table للفرز والتصفية وترقيم الصفحات.

npx shadcn@latest add table badge dropdown-menu
npm install @tanstack/react-table

أنشئ جدول بيانات لعرض قائمة المستخدمين:

// src/components/users-table.tsx
"use client"
 
import { useState } from "react"
import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getSortedRowModel,
  SortingState,
  useReactTable,
} from "@tanstack/react-table"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { ArrowUpDown } from "lucide-react"
 
type User = {
  id: string
  name: string
  email: string
  role: "admin" | "editor" | "viewer"
  status: "active" | "inactive"
}
 
const data: User[] = [
  {
    id: "1",
    name: "أحمد مارتن",
    email: "ahmed@example.com",
    role: "admin",
    status: "active",
  },
  {
    id: "2",
    name: "سارة جونسون",
    email: "sara@example.com",
    role: "editor",
    status: "active",
  },
  {
    id: "3",
    name: "كريم ويليامز",
    email: "karim@example.com",
    role: "viewer",
    status: "inactive",
  },
]
 
const columns: ColumnDef<User>[] = [
  {
    accessorKey: "name",
    header: ({ column }) => (
      <Button
        variant="ghost"
        onClick={() =>
          column.toggleSorting(column.getIsSorted() === "asc")
        }
      >
        الاسم
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: "email",
    header: "البريد الإلكتروني",
  },
  {
    accessorKey: "role",
    header: "الدور",
    cell: ({ row }) => {
      const role = row.getValue("role") as string
      const variant =
        role === "admin"
          ? "default"
          : role === "editor"
            ? "secondary"
            : "outline"
      return <Badge variant={variant}>{role}</Badge>
    },
  },
  {
    accessorKey: "status",
    header: "الحالة",
    cell: ({ row }) => {
      const status = row.getValue("status") as string
      return (
        <Badge
          variant={status === "active" ? "default" : "destructive"}
        >
          {status === "active" ? "نشط" : "غير نشط"}
        </Badge>
      )
    },
  },
]
 
export function UsersTable() {
  const [sorting, setSorting] = useState<SortingState>([])
  const [globalFilter, setGlobalFilter] = useState("")
 
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onSortingChange: setSorting,
    onGlobalFilterChange: setGlobalFilter,
    state: { sorting, globalFilter },
  })
 
  return (
    <div className="space-y-4">
      <Input
        placeholder="البحث عن مستخدم..."
        value={globalFilter}
        onChange={(e) => setGlobalFilter(e.target.value)}
        className="max-w-sm"
      />
      <div className="rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => (
                  <TableHead key={header.id}>
                    {header.isPlaceholder
                      ? null
                      : flexRender(
                          header.column.columnDef.header,
                          header.getContext()
                        )}
                  </TableHead>
                ))}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows.length > 0 ? (
              table.getRowModel().rows.map((row) => (
                <TableRow key={row.id}>
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(
                        cell.column.columnDef.cell,
                        cell.getContext()
                      )}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell
                  colSpan={columns.length}
                  className="h-24 text-center"
                >
                  لا توجد نتائج.
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>
    </div>
  )
}

الخطوة 8: تنفيذ الوضع الداكن

يدعم shadcn/ui الوضع الداكن بشكل أصلي من خلال متغيرات CSS. ثبّت next-themes لإدارة التبديل:

npm install next-themes
npx shadcn@latest add dropdown-menu

أنشئ مزود السمة:

// src/components/theme-provider.tsx
"use client"
 
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
 
export function ThemeProvider({
  children,
  ...props
}: React.ComponentProps<typeof NextThemesProvider>) {
  return (
    <NextThemesProvider {...props}>
      {children}
    </NextThemesProvider>
  )
}

أضفه إلى التخطيط الجذري:

// src/app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ar" dir="rtl" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

أنشئ زر تبديل السمة:

// src/components/theme-toggle.tsx
"use client"
 
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
 
export function ThemeToggle() {
  const { setTheme } = useTheme()
 
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
          <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
          <span className="sr-only">تغيير السمة</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onClick={() => setTheme("light")}>
          فاتح
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("dark")}>
          داكن
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("system")}>
          النظام
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

الخطوة 9: مكونات Dialog و Sheet

النوافذ المنبثقة واللوحات الجانبية أساسية في التطبيقات الحديثة:

npx shadcn@latest add dialog sheet alert-dialog

مثال على حوار التأكيد:

// src/components/confirm-dialog.tsx
"use client"
 
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
 
interface ConfirmDialogProps {
  title: string
  description: string
  onConfirm: () => void
  children: React.ReactNode
}
 
export function ConfirmDialog({
  title,
  description,
  onConfirm,
  children,
}: ConfirmDialogProps) {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        {children}
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>{title}</AlertDialogTitle>
          <AlertDialogDescription>
            {description}
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>إلغاء</AlertDialogCancel>
          <AlertDialogAction onClick={onConfirm}>
            تأكيد
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

الخطوة 10: تنظيم المكونات

للحفاظ على مشروع نظيف، اعتمد هذه البنية:

src/
├── components/
│   ├── ui/           # مكونات shadcn/ui (مُنشأة)
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   ├── dialog.tsx
│   │   └── ...
│   ├── forms/        # نماذج مُركبة
│   │   ├── contact-form.tsx
│   │   └── login-form.tsx
│   ├── tables/       # جداول البيانات
│   │   └── users-table.tsx
│   ├── layout/       # التنقل والتذييل وما إلى ذلك
│   │   ├── header.tsx
│   │   ├── sidebar.tsx
│   │   └── footer.tsx
│   └── shared/       # مكونات مخصصة قابلة لإعادة الاستخدام
│       ├── confirm-dialog.tsx
│       └── theme-toggle.tsx
├── hooks/            # خطافات مخصصة
│   └── use-toast.ts
└── lib/
    └── utils.ts      # أداة cn()

قواعد التنظيم:

  • لا تعدّل أبداً الملفات في ui/ لتغييرات خاصة بحالة استخدام معينة. أنشئ غلافاً في shared/ بدلاً من ذلك
  • ابدأ المكونات المُركبة بسياقها: ContactForm، UsersTable
  • صدّر من ملفات index إذا كان لديك الكثير من المكونات في مجلد

أفضل الممارسات

1. استخدم دالة cn() دائماً

import { cn } from "@/lib/utils"
 
function MyComponent({ className }: { className?: string }) {
  return (
    <div className={cn("p-4 rounded-lg", className)}>
      المحتوى
    </div>
  )
}

2. التركيب بدلاً من التكوين

// فضّل التركيب
<Card>
  <CardHeader>
    <CardTitle>العنوان</CardTitle>
  </CardHeader>
  <CardContent>المحتوى</CardContent>
</Card>
 
// بدلاً من الخصائص المعقدة
// <Card title="العنوان" content="المحتوى" /> // تجنب هذا

3. إمكانية الوصول المدمجة

shadcn/ui مبني على Radix UI، الذي يتعامل تلقائياً مع:

  • التنقل بلوحة المفاتيح
  • سمات ARIA
  • حصر التركيز في النوافذ المنبثقة
  • إعلانات قارئات الشاشة

تأكد دائماً من تضمين تسميات لحقول النماذج ونص بديل للعناصر المرئية.

4. الأداء مع React Server Components

يعمل shadcn/ui مع React Server Components. المكونات التي لا تتطلب تفاعلاً يمكن أن تبقى كمكونات خادم:

// يمكن أن يكون هذا Server Component
// لا حاجة لـ "use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
 
export function StatCard({ title, value }: { title: string; value: string }) {
  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-sm font-medium">{title}</CardTitle>
      </CardHeader>
      <CardContent>
        <p className="text-2xl font-bold">{value}</p>
      </CardContent>
    </Card>
  )
}

استكشاف الأخطاء

المكون لا يظهر بشكل صحيح

تحقق من أن tailwind.config.ts يتضمن مسارات المكونات:

content: [
  "./src/**/*.{ts,tsx}",
  "./components/**/*.{ts,tsx}",
],

خطأ "Module not found" بعد إضافة مكون

أعد تشغيل خادم التطوير. أحياناً لا يتم اكتشاف الملفات الجديدة بواسطة التحديث السريع:

# Ctrl+C ثم
npm run dev

أنماط الوضع الداكن لا تعمل

تأكد من وجود darkMode: "class" في tailwind.config.ts وأن ThemeProvider يُغلف تطبيقك.

الخطوات التالية

الآن بعد أن أتقنت أساسيات shadcn/ui مع Next.js، إليك كيفية المضي قدماً:

  • استكشف المزيد من المكونات: يقدم shadcn/ui أكثر من 40 مكوناً (Accordion، Calendar، Combobox، Command palette، إلخ)
  • أنشئ نظام تصميم: استخدم متغيرات CSS والمتغيرات لإنشاء هوية بصرية متسقة
  • تكامل مع API: اربط نماذجك وجداولك بواجهة API خلفية باستخدام TanStack Query
  • أضف الحركات: استخدم Framer Motion لانتقالات سلسة بين الحالات

الخلاصة

يمثل shadcn/ui تطوراً كبيراً في كيفية بناء الواجهات باستخدام React. من خلال منحك الملكية الكاملة للكود، يزيل قيود مكتبات المكونات التقليدية مع توفير أساس متين وسهل الوصول وقابل للتخصيص.

النقاط الرئيسية:

  • ملكية الكود: تتحكم في كل جانب من مكوناتك
  • إمكانية الوصول الأصلية: Radix UI يتعامل مع التفاعلات المعقدة
  • سمات مرنة: متغيرات CSS تتيح تخصيصاً كاملاً
  • تكامل TypeScript: أنواع صارمة لتجربة مطور أفضل
  • نظام بيئي غني: React Hook Form و Zod و TanStack Table تتكامل بشكل طبيعي

مع هذا الأساس، أنت مستعد لبناء تطبيقات احترافية بواجهات مستخدم عالية الجودة.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على Upstash Redis و Next.js: تحديد معدل الطلبات والتخزين المؤقت وقوائم الرسائل.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار

تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.

30 د قراءة·