לוח שנה
בוחר תאריכים בתצוגת לוח חודשי להזמנות, תזמון ובחירת טווח — עם עיצוב ניאו-ברוטליסטי הכולל מסגרות וצללים.
"use client"
import * as React from "react"ה-calendar מצייר רשת חודש לבחירת תאריך בודד, כמה תאריכים או טווח. נבנה על React DayPicker — שני הווריאנטים של ה-backend עוטפים את אותה ספרייה — ומעוצב לפי המתכון הנאו-ברוטליסטי: מסגרות עבות, צללים חדים וטיפוגרפיה מודגשת.
השתמשו בו כש:
- שדות תאריך בטפסים — חברו אותו ל-popover כדי לבנות Date Picker לימי הולדת, דדליינים ותאריכי משלוח.
- זרימות הזמנה —
mode="range"ל-check-in/check-out,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 כדי להבטיח תאימות עם רינדור בצד השרת. זיהוי אזור הזמן במהלך הרינדור היה גורם לחוסר התאמה בהידרציה, מכיוון שהשרת והלקוח עשויים להיות באזורי זמן שונים.
דוגמאות#
בסיסי#
רכיב לוח שנה בסיסי. השתמשנו ב-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"
/>נגישות#
ה-calendar עוקב אחרי מודל ניווט הרשת מ-דוגמת Date Picker Dialog של WAI-ARIA: React DayPicker מרנדר את החודש כ-role="grid" עם כל יום ככפתור אמיתי, ומכריז על ימים נבחרים ומושבתים לקוראי מסך. שני הווריאנטים של ה-backend חולקים את ההתנהגות הזו כי הם עוטפים את אותה ספרייה.
אינטראקציות מקלדת:
| מקש | פעולה |
|---|---|
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 למאפייני הרכיב:
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} />