Kalendarz
Wybór daty w układzie miesięcznym do rezerwacji, planowania i zaznaczania przedziałów czasowych — z neobrutalistycznymi ramkami i cieniami.
"use client"
import * as React from "react"Kalendarz renderuje siatkę miesiąca do wyboru pojedynczej daty, wielu dat lub zakresu. Zbudowany na React DayPicker — oba warianty backendów opakowują tę samą bibliotekę — i ostylowany zgodnie z neobrutalistyczną recepturą: grube obramowania, twarde cienie i wyrazista typografia.
Sięgnij po ten komponent, gdy:
- Pola dat w formularzach — połącz go z popoverem, aby zbudować Date Picker do dat urodzenia, terminów i dostaw.
- Procesy rezerwacyjne —
mode="range"dla zameldowania i wymeldowania,disableddla dni już zajętych. - Filtry na pulpitach — zakresy dat raportów, widoki dostępności i gotowe skróty w rodzaju „ostatnie 7 dni”.
Instalacja#
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
Zainstaluj następujące zależności:
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
Dodaj komponent Button do swojego projektu.
Komponent Calendar używa komponentu Button. Upewnij się, że masz go zainstalowanego w swoim projekcie.
Skopiuj i wklej poniższy kod do swojego projektu.
"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 }
Zaktualizuj ścieżki importów, aby pasowały do struktury twojego projektu.
Użycie#
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"
/>
)Więcej informacji znajdziesz w dokumentacji React DayPicker.
Informacje#
Komponent Calendar jest zbudowany na React DayPicker.
Date Picker#
Możesz użyć komponentu <Calendar>, aby zbudować date picker. Więcej informacji znajdziesz na stronie Date Picker.
Kalendarz perski / hidżri / dżalali#
Aby użyć kalendarza perskiego, edytuj components/ui/calendar.tsx i zamień react-day-picker na react-day-picker/persian.
- import { DayPicker } from "react-day-picker"
+ import { DayPicker } from "react-day-picker/persian""use client"
import * as React from "react"Wybrana data (ze strefą czasową)#
Komponent Calendar przyjmuje prop timeZone, aby zapewnić, że daty są wyświetlane i wybierane w lokalnej strefie czasowej użytkownika.
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}
/>
)
}Uwaga: Jeśli zauważysz przesunięcie wybranej daty (na przykład wybranie 20. podświetla 19.), upewnij się, że prop timeZone jest ustawiony na lokalną strefę czasową użytkownika.
Dlaczego po stronie klienta? Strefa czasowa jest wykrywana za pomocą Intl.DateTimeFormat().resolvedOptions().timeZone wewnątrz useEffect, aby zapewnić zgodność z renderowaniem po stronie serwera. Wykrywanie strefy czasowej podczas renderowania powodowałoby niezgodności hydracji, ponieważ serwer i klient mogą znajdować się w różnych strefach czasowych.
Przykłady#
Podstawowy#
Podstawowy komponent kalendarza. Użyliśmy className="rounded border-2 shadow-md", aby ostylować kalendarz.
"use client"
import { Calendar } from "@/components/ui/calendar"Kalendarz zakresu#
Użyj propa mode="range", aby włączyć wybór zakresu.
"use client"
import * as React from "react"Wybór miesiąca i roku#
Użyj captionLayout="dropdown", aby pokazać rozwijane listy miesiąca i roku.
"use client"
import { Calendar } from "@/components/ui/calendar"Presety#
"use client"
import * as React from "react"Wybór daty i godziny#
"use client"
import * as React from "react"Zarezerwowane daty#
"use client"
import * as React from "react"Niestandardowy rozmiar komórki#
"use client"
import * as React from "react"Możesz dostosować rozmiar komórek kalendarza za pomocą zmiennej CSS --cell-size. Możesz również sprawić, by był responsywny, używając wartości specyficznych dla poszczególnych breakpointów:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>Albo użyj wartości stałych:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>Numery tygodni#
Użyj showWeekNumber, aby pokazać numery tygodni.
"use client"
import * as React from "react"RTL#
Aby włączyć obsługę RTL w Neobrutalism, zobacz przewodnik konfiguracji RTL.
Zobacz też przewodnik hidżri, aby włączyć kalendarz perski / hidżri / dżalali.
"use client"
import * as React from "react"Podczas używania RTL zaimportuj locale z react-day-picker/locale i przekaż komponentowi Calendar zarówno prop locale, jak i prop dir:
import { arSA } from "react-day-picker/locale"
;<Calendar
mode="single"
selected={date}
onSelect={setDate}
locale={arSA}
dir="rtl"
/>Dostępność#
Kalendarz realizuje model nawigacji po siatce ze wzorca WAI-ARIA Date Picker Dialog: React DayPicker renderuje miesiąc jako role="grid", w którym każdy dzień jest prawdziwym przyciskiem, i ogłasza czytnikom ekranu dni wybrane oraz wyłączone. Oba warianty backendów dzielą to zachowanie, bo opakowują tę samą bibliotekę.
Obsługa klawiatury:
| Klawisz | Działanie |
|---|---|
Tab / Shift + Tab | Przenosi fokus między przyciskami nawigacji a siatką dni |
ArrowLeft / ArrowRight | Przenosi fokus na poprzedni / następny dzień |
ArrowUp / ArrowDown | Przenosi fokus na ten sam dzień tygodnia w poprzednim / następnym tygodniu |
Home / End | Przenosi fokus na pierwszy / ostatni dzień tygodnia |
PageUp / PageDown | Przechodzi do poprzedniego / następnego miesiąca |
Shift + PageUp / Shift + PageDown | Przechodzi do poprzedniego / następnego roku |
Space / Enter | Wybiera dzień z fokusem |
Referencja API#
Więcej informacji o komponencie Calendar znajdziesz w dokumentacji React DayPicker.
Lista zmian#
Obsługa RTL#
Jeśli aktualizujesz z wcześniejszej wersji komponentu Calendar, musisz zastosować następujące zmiany, aby dodać obsługę locale:
Zaimportuj typ Locale.
Dodaj Locale do swoich importów z react-day-picker:
import {
DayPicker,
getDefaultClassNames,
type DayButton,
+ type Locale,
} from "react-day-picker"Dodaj prop locale do komponentu Calendar.
Dodaj prop locale do propów komponentu:
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
+ locale,
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {Przekaż locale do DayPicker.
Przekaż prop locale do komponentu 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,
}}Zaktualizuj CalendarDayButton, aby przyjmował locale.
Zaktualizuj sygnaturę komponentu CalendarDayButton i przekaż locale:
function CalendarDayButton({
className,
day,
modifiers,
+ locale,
...props
- }: React.ComponentProps<typeof DayButton>) {
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {Zaktualizuj formatowanie dat w CalendarDayButton.
Użyj locale?.code w formatowaniu dat:
<Button
variant="ghost"
size="icon"
- data-day={day.date.toLocaleDateString()}
+ data-day={day.date.toLocaleDateString(locale?.code)}
...
/>Przekaż locale do komponentu DayButton.
Zaktualizuj użycie komponentu DayButton, aby przekazać prop locale:
components={{
...
- DayButton: CalendarDayButton,
+ DayButton: ({ ...props }) => (
+ <CalendarDayButton locale={locale} {...props} />
+ ),
...
}}Zaktualizuj klasy CSS uwzględniające RTL.
Zastąp klasy kierunkowe właściwościami logicznymi, aby uzyskać lepszą obsługę 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)Po zastosowaniu tych zmian możesz użyć propa locale, aby zapewnić formatowanie specyficzne dla danego locale:
import { enUS } from "react-day-picker/locale"
;<Calendar mode="single" selected={date} onSelect={setDate} locale={enUS} />