Skip to content

Commit cffcfc5

Browse files
authored
Merge pull request #101 from Logging-Studio/calender
New calendar component!
2 parents 7e4cab7 + 0c669f9 commit cffcfc5

File tree

14 files changed

+355
-10
lines changed

14 files changed

+355
-10
lines changed

components/retroui/Button.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { cva, VariantProps } from "class-variance-authority";
33
import React, { ButtonHTMLAttributes } from "react";
44
import { Slot } from "@radix-ui/react-slot";
55

6-
const buttonVariants = cva(
6+
export const buttonVariants = cva(
77
"font-head transition-all rounded outline-hidden cursor-pointer duration-200 font-medium flex items-center",
88
{
99
variants: {
@@ -15,6 +15,7 @@ const buttonVariants = cva(
1515
outline:
1616
"shadow-md hover:shadow active:shadow-none bg-transparent border-2 transition hover:translate-y-1 active:translate-y-2 active:translate-x-1",
1717
link: "bg-transparent hover:underline",
18+
ghost: "bg-transparent hover:bg-accent"
1819
},
1920
size: {
2021
sm: "px-3 py-1 text-sm shadow hover:shadow-none",

components/retroui/Calendar.tsx

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"use client"
2+
3+
import * as React from "react"
4+
import {
5+
ChevronDownIcon,
6+
ChevronLeftIcon,
7+
ChevronRightIcon,
8+
} from "lucide-react"
9+
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
10+
11+
import { cn } from "@/lib/utils"
12+
import { Button, buttonVariants } from "@/components/retroui/Button"
13+
14+
function Calendar({
15+
className,
16+
classNames,
17+
showOutsideDays = true,
18+
captionLayout = "label",
19+
buttonVariant = "ghost",
20+
formatters,
21+
components,
22+
...props
23+
}: React.ComponentProps<typeof DayPicker> & {
24+
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
25+
}) {
26+
const defaultClassNames = getDefaultClassNames()
27+
28+
return (
29+
<DayPicker
30+
showOutsideDays={showOutsideDays}
31+
className={cn(
32+
"bg-background w-full outline-2 shadow-md group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
33+
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
34+
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
35+
className
36+
)}
37+
captionLayout={captionLayout}
38+
formatters={{
39+
formatMonthDropdown: (date) =>
40+
date.toLocaleString("default", { month: "short" }),
41+
...formatters,
42+
}}
43+
classNames={{
44+
root: cn("w-fit", defaultClassNames.root),
45+
months: cn(
46+
"flex gap-4 flex-col md:flex-row relative",
47+
defaultClassNames.months
48+
),
49+
month: cn("flex flex-col w-full gap-4 font-head", defaultClassNames.month),
50+
nav: cn(
51+
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
52+
defaultClassNames.nav
53+
),
54+
button_previous: cn(
55+
buttonVariants({ variant: buttonVariant }),
56+
"size-8 p-2 border-2 rounded select-none",
57+
defaultClassNames.button_previous
58+
),
59+
button_next: cn(
60+
buttonVariants({ variant: buttonVariant }),
61+
"size-8 p-2 border-2 rounded select-none",
62+
defaultClassNames.button_next
63+
),
64+
month_caption: cn(
65+
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
66+
defaultClassNames.month_caption
67+
),
68+
dropdowns: cn(
69+
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
70+
defaultClassNames.dropdowns
71+
),
72+
dropdown_root: cn(
73+
"relative has-focus:outline-ring outline outline-input has-focus:ring-ring/50 has-focus:ring-[3px] rounded",
74+
defaultClassNames.dropdown_root
75+
),
76+
dropdown: cn(
77+
"absolute bg-popover inset-0 opacity-0",
78+
defaultClassNames.dropdown
79+
),
80+
caption_label: cn(
81+
"select-none font-medium",
82+
captionLayout === "label"
83+
? "text-base"
84+
: "rounded-none pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
85+
defaultClassNames.caption_label
86+
),
87+
table: "w-full outline-collapse",
88+
weekdays: cn("flex", defaultClassNames.weekdays),
89+
weekday: cn(
90+
"flex-1 font-normal text-sm select-none",
91+
defaultClassNames.weekday
92+
),
93+
week: cn("flex w-full mt-2", defaultClassNames.week),
94+
week_number_header: cn(
95+
"select-none w-(--cell-size)",
96+
defaultClassNames.week_number_header
97+
),
98+
week_number: cn(
99+
"text-[0.8rem] select-none text-muted-foreground",
100+
defaultClassNames.week_number
101+
),
102+
day: cn(
103+
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r group/day aspect-square select-none",
104+
props.showWeekNumber
105+
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l"
106+
: "[&:first-child[data-selected=true]_button]:rounded-l",
107+
defaultClassNames.day
108+
),
109+
today: cn(
110+
"bg-accent text-accent-foreground rounded data-[selected=true]:rounded-none",
111+
defaultClassNames.today
112+
),
113+
outside: cn(
114+
"text-muted-foreground aria-selected:text-muted-foreground opacity-80",
115+
defaultClassNames.outside
116+
),
117+
disabled: cn(
118+
"text-muted-foreground opacity-50",
119+
defaultClassNames.disabled
120+
),
121+
hidden: cn("invisible", defaultClassNames.hidden),
122+
...classNames,
123+
}}
124+
components={{
125+
Root: ({ className, rootRef, ...props }) => {
126+
return (
127+
<div
128+
data-slot="calendar"
129+
ref={rootRef}
130+
className={cn(className)}
131+
{...props}
132+
/>
133+
)
134+
},
135+
Chevron: ({ className, orientation, ...props }) => {
136+
if (orientation === "left") {
137+
return (
138+
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
139+
)
140+
}
141+
142+
if (orientation === "right") {
143+
return (
144+
<ChevronRightIcon
145+
className={cn("size-4", className)}
146+
{...props}
147+
/>
148+
)
149+
}
150+
151+
return (
152+
<ChevronDownIcon className={cn("size-4", className)} {...props} />
153+
)
154+
},
155+
DayButton: CalendarDayButton,
156+
WeekNumber: ({ children, ...props }) => {
157+
return (
158+
<td {...props}>
159+
<div className="flex size-(--cell-size) items-center justify-center text-center">
160+
{children}
161+
</div>
162+
</td>
163+
)
164+
},
165+
...components,
166+
}}
167+
{...props}
168+
/>
169+
)
170+
}
171+
172+
function CalendarDayButton({
173+
className,
174+
day,
175+
modifiers,
176+
...props
177+
}: React.ComponentProps<typeof DayButton>) {
178+
const defaultClassNames = getDefaultClassNames()
179+
180+
const ref = React.useRef<HTMLButtonElement>(null)
181+
React.useEffect(() => {
182+
if (modifiers.focused) ref.current?.focus()
183+
}, [modifiers.focused])
184+
185+
return (
186+
<Button
187+
ref={ref}
188+
variant="ghost"
189+
size="icon"
190+
data-day={day.date.toLocaleDateString()}
191+
data-selected-single={
192+
modifiers.selected &&
193+
!modifiers.range_start &&
194+
!modifiers.range_end &&
195+
!modifiers.range_middle
196+
}
197+
data-range-start={modifiers.range_start}
198+
data-range-end={modifiers.range_end}
199+
data-range-middle={modifiers.range_middle}
200+
className={cn(
201+
"font-sans flex justify-center items-center data-[selected-single=true]:shadow-md data-[selected-single=true]:outline-2 outline-border data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-secondary data-[range-middle=true]:hover:text-secondary-foreground data-[range-middle=true]:text-secondary-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring-1 group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[2px] data-[range-end=true]:rounded-none data-[range-end=true]:rounded-none data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-none [&>span]:text-xs",
202+
defaultClassNames.day,
203+
className
204+
)}
205+
{...props}
206+
/>
207+
)
208+
}
209+
210+
export { Calendar, CalendarDayButton }

config/components.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ export const componentConfig: {
286286
() => import("@/preview/components/button-style-with-icon"),
287287
),
288288
},
289+
"calendar-style-default": {
290+
name: "calendar-style-default",
291+
filePath: "preview/components/calendar-style-default.tsx",
292+
preview: lazy(() => import("@/preview/components/calendar-style-default")),
293+
},
289294
"card-style-default": {
290295
name: "card-style-default",
291296
filePath: "preview/components/card-style-default.tsx",

config/navigation.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ export const navConfig: INavigationConfig = {
4949
{ title: "Avatar", href: `${componentsRoute}/avatar` },
5050
{ title: "Badge", href: `${componentsRoute}/badge` },
5151
{ title: "Breadcrumb", href: `${componentsRoute}/breadcrumb` },
52-
{ title: "Button", href: `${componentsRoute}/button`, tag: "Updated" },
52+
{ title: "Button", href: `${componentsRoute}/button` },
5353
{ title: "Card", href: `${componentsRoute}/card` },
54+
{ title: "Calendar", href: `${componentsRoute}/calendar`, tag: "New" },
5455
{ title: "Checkbox", href: `${componentsRoute}/checkbox` },
5556
{ title: "Command", href: `${componentsRoute}/command` },
5657
{ title: "Dialog", href: `${componentsRoute}/dialog` },
@@ -93,10 +94,10 @@ export const navConfig: INavigationConfig = {
9394
{
9495
title: "Chart",
9596
children: [
96-
{ title: "Bar Chart", href: `${chartsRoute}/bar-chart`, tag: "New" },
97-
{ title: "Line Chart", href: `${chartsRoute}/line-chart`, tag: "New" },
98-
{ title: "Area Chart", href: `${chartsRoute}/area-chart`, tag: "New" },
99-
{ title: "Pie Chart", href: `${chartsRoute}/pie-chart`, tag: "New" },
97+
{ title: "Bar Chart", href: `${chartsRoute}/bar-chart` },
98+
{ title: "Line Chart", href: `${chartsRoute}/line-chart` },
99+
{ title: "Area Chart", href: `${chartsRoute}/area-chart` },
100+
{ title: "Pie Chart", href: `${chartsRoute}/pie-chart` },
100101
],
101102
},
102103
{
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
title: Calendar
3+
description: Let your users select a date to cancel subscription.
4+
lastUpdated: 14 Nov, 2025
5+
---
6+
7+
<ComponentShowcase name="calendar-style-default" />
8+
<br />
9+
<br />
10+
11+
## Installation
12+
13+
<ComponentInstall>
14+
<ComponentInstall.Cli npmCommand="npx shadcn@latest add @retroui/calendar" />
15+
<ComponentInstall.Manual>
16+
17+
#### 1. Install dependencies:
18+
19+
```sh
20+
npm install react-day-picker lucide-react
21+
```
22+
23+
<br />
24+
25+
#### 2. Copy the code 👇 into your project:
26+
27+
<ComponentSource name="calendar" />
28+
29+
</ComponentInstall.Manual>
30+
</ComponentInstall>
31+
32+
<br />
33+
<br />
34+
35+
## Examples
36+
37+
### Default
38+
39+
<ComponentShowcase name="calendar-style-default" />

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"next": "14.2.7",
4242
"next-contentlayer": "^0.3.4",
4343
"react": "^18",
44+
"react-day-picker": "^9.11.1",
4445
"react-dom": "^18",
4546
"recharts": "^3.1.2",
4647
"rehype-pretty-code": "^0.14.0",

pnpm-lock.yaml

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)