تقويم
أداة اختيار التواريخ بنظام الشبكة الشهرية للحجوزات، وتحديد المواعيد، واختيار النطاقات — مع تصميم «النيوبروتاليستي» الذي يتميز بالحدود والظلال.
"use client"
import * as React from "react"يعرض التقويم شبكة شهرية لاختيار تاريخ واحد أو عدة تواريخ أو نطاق زمني. وقد تم تطويره باستخدام React DayPicker — حيث يستخدم كلا النسختين في الخلفية نفس المكتبة — وتم تصميمه وفقًا لأسلوب «النيوبروتالية»: حدود سميكة، وظلال حادة، وخط عريض.
استخدمه عندما:
- حقول التاريخ في النماذج — ادمجه مع نافذة منبثقة لإنشاء Date Picker لأعياد الميلاد والمواعيد النهائية وتواريخ التسليم.
- عمليات الحجز —
mode="range"لتسجيل الوصول/المغادرة، وdisabledللأيام المحجوزة مسبقًا. - مرشحات لوحة التحكم — نطاقات تواريخ التقارير، وعروض التوافر، والاختصارات المعدة مسبقًا مثل «آخر 7 أيام».
التثبيت#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/calendar.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/calendar.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/calendar.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/calendar.json
ثبّت التبعيات التالية:
pnpm add react-day-picker date-fnsnpm install react-day-picker date-fnsyarn add react-day-picker date-fnsbun add react-day-picker date-fns
أضف مكوّن Button إلى مشروعك.
يستخدم مكوّن Calendar مكوّن Button. تأكّد من أنّك ثبّته في مشروعك.
انسخ الشيفرة التالية والصقها في مشروعك.
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import {
DayPicker,
getDefaultClassNames,
type DayButton,
type Locale,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
fixedWeeks = true,
captionLayout = "label",
buttonVariant = "ghost",
locale,
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
fixedWeeks={fixedWeeks}
className={cn(
"group/calendar bg-card p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
locale={locale}
formatters={{
formatMonthDropdown: (date, dateLib) =>
dateLib
? dateLib.format(date, "LLL")
: date.toLocaleString(locale?.code, { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) rounded border-2 p-0 shadow-sm select-none aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) rounded border-2 p-0 shadow-sm select-none aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"cn-calendar-dropdown-root relative rounded-(--cell-radius)",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute inset-0 bg-popover opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"font-head font-medium select-none",
captionLayout === "label"
? "cn-calendar-caption text-sm"
: "cn-calendar-caption-label flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
defaultClassNames.caption_label
),
month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"flex w-(--cell-size) items-center justify-center rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-(--cell-size) select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] text-muted-foreground select-none",
defaultClassNames.week_number
),
day: cn(
"group/day relative size-(--cell-size) rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
defaultClassNames.day
),
range_start: cn(
"relative isolate z-0 rounded-l-(--cell-radius) bg-primary/30 after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-primary/30",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn(
"relative isolate z-0 rounded-r-(--cell-radius) bg-primary/30 after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-primary/30",
defaultClassNames.range_end
),
today: cn(
"rounded-(--cell-radius) border-2 border-primary text-foreground data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon
className={cn("cn-rtl-flip size-4", className)}
{...props}
/>
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("cn-rtl-flip size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: ({ ...props }) => (
<CalendarDayButton locale={locale} {...props} />
),
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
locale,
...props
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={`${day.date.getFullYear()}-${String(
day.date.getMonth() + 1
).padStart(2, "0")}-${String(day.date.getDate()).padStart(2, "0")}`}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"relative isolate z-10 flex size-(--cell-size) flex-col items-center justify-center gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:outline-2 group-data-[focused=true]/day:outline-offset-2 group-data-[focused=true]/day:outline-primary data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:border-2 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-primary/30 data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:border-2 data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:border-2 data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }
حدّث مسارات الاستيراد لتطابق إعداد مشروعك.
الاستخدام#
import { Calendar } from "@/components/ui/calendar"const [date, setDate] = React.useState<Date | undefined>(new Date())
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md"
/>
)لمزيد من المعلومات، راجع وثائق React DayPicker.
نبذة#
مكوّن Calendar مبني على React DayPicker.
Date Picker#
يمكنك استخدام مكوّن <Calendar> لبناء منتقي تاريخ. راجع صفحة Date Picker لمزيد من المعلومات.
تقويم فارسي / هجري / جلالي#
لاستخدام التقويم الفارسي، عدّل components/ui/calendar.tsx واستبدل react-day-picker بـ react-day-picker/persian.
- import { DayPicker } from "react-day-picker"
+ import { DayPicker } from "react-day-picker/persian""use client"
import * as React from "react"التاريخ المحدد (مع المنطقة الزمنية)#
يقبل مكوّن Calendar خاصية timeZone لضمان عرض التواريخ وتحديدها في المنطقة الزمنية المحلية للمستخدم.
export function CalendarWithTimezone() {
const [date, setDate] = React.useState<Date | undefined>(undefined)
const [timeZone, setTimeZone] = React.useState<string | undefined>(undefined)
React.useEffect(() => {
setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone)
}, [])
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
timeZone={timeZone}
/>
)
}ملاحظة: إذا لاحظت إزاحة في التاريخ المحدد (على سبيل المثال، تحديد يوم 20 يبرز يوم 19)، فتأكّد من ضبط خاصية timeZone على المنطقة الزمنية المحلية للمستخدم.
لماذا في جانب العميل؟ تُكتشف المنطقة الزمنية باستخدام Intl.DateTimeFormat().resolvedOptions().timeZone داخل useEffect لضمان التوافق مع العرض من جانب الخادم. اكتشاف المنطقة الزمنية أثناء العرض سيؤدّي إلى عدم تطابق أثناء الإماهة (hydration)، لأنّ الخادم والعميل قد يكونان في مناطق زمنية مختلفة.
أمثلة#
أساسي#
مكوّن تقويم أساسي. استخدمنا className="rounded border-2 shadow-md" لتنسيق التقويم.
"use client"
import { Calendar } from "@/components/ui/calendar"تقويم النطاق#
استخدم خاصية mode="range" لتفعيل تحديد النطاق.
"use client"
import * as React from "react"محدّد الشهر والسنة#
استخدم captionLayout="dropdown" لعرض قوائم منسدلة للشهر والسنة.
"use client"
import { Calendar } from "@/components/ui/calendar"الإعدادات المسبقة#
"use client"
import * as React from "react"منتقي التاريخ والوقت#
"use client"
import * as React from "react"التواريخ المحجوزة#
"use client"
import * as React from "react"حجم خلية مخصص#
"use client"
import * as React from "react"يمكنك تخصيص حجم خلايا التقويم باستخدام متغيّر CSS --cell-size. يمكنك أيضًا جعله متجاوبًا باستخدام قيم خاصة بنقاط التوقّف:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>أو استخدم قيمًا ثابتة:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>أرقام الأسابيع#
استخدم showWeekNumber لعرض أرقام الأسابيع.
"use client"
import * as React from "react"RTL#
لتفعيل RTL في Neobrutalism، راجع دليل إعداد RTL.
راجع أيضًا دليل الهجري لتفعيل التقويم الفارسي / الهجري / الجلالي.
"use client"
import * as React from "react"عند استخدام RTL، استورد الـ locale من react-day-picker/locale ومرّر خاصيتَي locale وdir كلتيهما إلى مكوّن Calendar:
import { arSA } from "react-day-picker/locale"
;<Calendar
mode="single"
selected={date}
onSelect={setDate}
locale={arSA}
dir="rtl"
/>إمكانية الوصول#
يتبع التقويم نموذج تنقل الشبكة من مثال WAI-ARIA Date Picker Dialog: يعرض React DayPicker الشهر كـ role="grid" وكل يوم زر حقيقي، ويعلن الأيام المحددة والمعطّلة لقارئات الشاشة. تشترك نسختا الخلفية في هذا السلوك لأنهما تغلفان المكتبة نفسها.
تفاعلات لوحة المفاتيح:
| المفتاح | الإجراء |
|---|---|
Tab / Shift + Tab | نقل التركيز بين أزرار التنقل وشبكة الأيام |
ArrowLeft / ArrowRight | نقل التركيز إلى اليوم السابق / التالي |
ArrowUp / ArrowDown | نقل التركيز إلى نفس يوم الأسبوع في الأسبوع السابق / التالي |
Home / End | نقل التركيز إلى أول / آخر يوم في الأسبوع |
PageUp / PageDown | الانتقال إلى الشهر السابق / التالي |
Shift + PageUp / Shift + PageDown | الانتقال إلى السنة السابقة / التالية |
Space / Enter | اختيار اليوم المركَّز |
مرجع API#
لمزيد من المعلومات حول مكوّن Calendar، راجع وثائق React DayPicker.
Changelog#
دعم RTL#
إذا كنت تُحدّث من إصدار سابق من مكوّن Calendar، فسيتعيّن عليك تطبيق التحديثات التالية لإضافة دعم locale:
استورد النوع Locale.
أضف Locale إلى استيراداتك من react-day-picker:
import {
DayPicker,
getDefaultClassNames,
type DayButton,
+ type Locale,
} from "react-day-picker"أضف خاصية locale إلى مكوّن Calendar.
أضف خاصية locale إلى props المكوّن:
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
+ locale,
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {مرّر locale إلى DayPicker.
مرّر خاصية locale إلى مكوّن DayPicker:
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(...)}
captionLayout={captionLayout}
+ locale={locale}
formatters={{
formatMonthDropdown: (date) =>
- date.toLocaleString("default", { month: "short" }),
+ date.toLocaleString(locale?.code, { month: "short" }),
...formatters,
}}حدّث CalendarDayButton ليقبل locale.
حدّث توقيع مكوّن CalendarDayButton ومرّر locale:
function CalendarDayButton({
className,
day,
modifiers,
+ locale,
...props
- }: React.ComponentProps<typeof DayButton>) {
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {حدّث تنسيق التاريخ في CalendarDayButton.
استخدم locale?.code في تنسيق التاريخ:
<Button
variant="ghost"
size="icon"
- data-day={day.date.toLocaleDateString()}
+ data-day={day.date.toLocaleDateString(locale?.code)}
...
/>مرّر locale إلى مكوّن DayButton.
حدّث استخدام مكوّن DayButton لتمرير خاصية locale:
components={{
...
- DayButton: CalendarDayButton,
+ DayButton: ({ ...props }) => (
+ <CalendarDayButton locale={locale} {...props} />
+ ),
...
}}حدّث فئات CSS المدركة لـ RTL.
استبدل الفئات الاتجاهية بخصائص منطقية لدعم RTL أفضل:
// In the day classNames:
- [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)
+ [&:last-child[data-selected=true]_button]:rounded-e-(--cell-radius)
- [&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)
+ [&:nth-child(2)[data-selected=true]_button]:rounded-s-(--cell-radius)
- [&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)
+ [&:first-child[data-selected=true]_button]:rounded-s-(--cell-radius)
// In range_start classNames:
- rounded-l-(--cell-radius) ... after:right-0
+ rounded-s-(--cell-radius) ... after:end-0
// In range_end classNames:
- rounded-r-(--cell-radius) ... after:left-0
+ rounded-e-(--cell-radius) ... after:start-0
// In CalendarDayButton className:
- data-[range-end=true]:rounded-r-(--cell-radius)
+ data-[range-end=true]:rounded-e-(--cell-radius)
- data-[range-start=true]:rounded-l-(--cell-radius)
+ data-[range-start=true]:rounded-s-(--cell-radius)بعد تطبيق هذه التغييرات، يمكنك استخدام خاصية locale لتوفير تنسيق خاص بالـ locale:
import { enUS } from "react-day-picker/locale"
;<Calendar mode="single" selected={date} onSelect={setDate} locale={enUS} />