تقویم
یک انتخابگر تاریخ در قالب جدول ماهانه برای رزرو، زمانبندی و انتخاب بازه — با جلوهی نئوبروتالیستی مرزها و سایهها.
"use client"
import * as React from "react"تقویم یک شبکهٔ ماه برای انتخاب یک تاریخ، چند تاریخ یا یک بازه رندر میکند. بر پایهٔ React DayPicker — هر دو variant بکاند همین کتابخانه را میپیچند — و با دستور پخت نئوبروتالیستی استایل شده: حاشیههای ضخیم، سایههای سخت و تایپوگرافی پررنگ.
برای این موارد از آن استفاده کنید:
- فیلدهای تاریخ فرم — با popover جفتش کنید تا 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 را متناسب با ساختار پروژهٔ خود بهروزرسانی کنید.
استفاده#
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 تشخیص داده میشود تا سازگاری با رندر سمت سرور تضمین شود. تشخیص منطقهٔ زمانی هنگام رندر باعث ناهماهنگی هیدراسیون میشود، زیرا سرور و کلاینت ممکن است در مناطق زمانی متفاوتی باشند.
نمونهها#
پایه#
یک کامپوننت تقویم پایه. ما از 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 import کنید و هر دو پراپ 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" رندر میکند، هر روز یک دکمهٔ واقعی است، و روزهای انتخابشده و غیرفعال را به صفحهخوان اعلام میکند. هر دو variant بکاند همین رفتار را دارند چون همان کتابخانه را میپیچند.
تعاملات صفحهکلید:
| کلید | عمل |
|---|---|
Tab / Shift + Tab | جابهجایی فوکوس بین دکمههای nav و شبکهٔ روزها |
ArrowLeft / ArrowRight | انتقال فوکوس به روز قبلی / بعدی |
ArrowUp / ArrowDown | انتقال فوکوس به همان روز هفته در هفتهٔ قبلی / بعدی |
Home / End | انتقال فوکوس به اولین / آخرین روز هفته |
PageUp / PageDown | رفتن به ماه قبلی / بعدی |
Shift + PageUp / Shift + PageDown | رفتن به سال قبلی / بعدی |
Space / Enter | انتخاب روز فوکوسشده |
مرجع API#
برای اطلاعات بیشتر دربارهٔ کامپوننت Calendar، مستندات React DayPicker را ببینید.
تاریخچهٔ تغییرات#
پشتیبانی RTL#
اگر از نسخهٔ قبلی کامپوننت Calendar ارتقا میدهید، باید تغییرات زیر را برای افزودن پشتیبانی locale اعمال کنید:
تایپ Locale را import کنید.
Locale را به importهای خود از react-day-picker اضافه کنید:
import {
DayPicker,
getDefaultClassNames,
type DayButton,
+ type Locale,
} from "react-day-picker"پراپ locale را به کامپوننت Calendar اضافه کنید.
پراپ locale را به پراپهای کامپوننت اضافه کنید:
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} />