@srcroot/ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +640 -0
- package/package.json +43 -0
- package/registry/accordion.tsx +158 -0
- package/registry/alert-dialog.tsx +206 -0
- package/registry/alert.tsx +73 -0
- package/registry/aspect-ratio.tsx +44 -0
- package/registry/avatar.tsx +94 -0
- package/registry/badge.tsx +68 -0
- package/registry/breadcrumb.tsx +151 -0
- package/registry/button-group.tsx +84 -0
- package/registry/button.tsx +102 -0
- package/registry/calendar.tsx +238 -0
- package/registry/card.tsx +114 -0
- package/registry/carousel.tsx +169 -0
- package/registry/checkbox.tsx +79 -0
- package/registry/collapsible.tsx +110 -0
- package/registry/container.tsx +60 -0
- package/registry/dialog.tsx +264 -0
- package/registry/dropdown-menu.tsx +387 -0
- package/registry/image.tsx +144 -0
- package/registry/input.tsx +44 -0
- package/registry/label.tsx +34 -0
- package/registry/loading-spinner.tsx +108 -0
- package/registry/otp-input.tsx +152 -0
- package/registry/pagination.tsx +146 -0
- package/registry/popover.tsx +135 -0
- package/registry/progress.tsx +49 -0
- package/registry/radio.tsx +99 -0
- package/registry/search.tsx +146 -0
- package/registry/select.tsx +190 -0
- package/registry/separator.tsx +44 -0
- package/registry/sheet.tsx +180 -0
- package/registry/skeleton.tsx +26 -0
- package/registry/slider.tsx +115 -0
- package/registry/star-rating.tsx +131 -0
- package/registry/switch.tsx +70 -0
- package/registry/table.tsx +136 -0
- package/registry/tabs.tsx +122 -0
- package/registry/text.tsx +70 -0
- package/registry/textarea.tsx +39 -0
- package/registry/toast.tsx +95 -0
- package/registry/tooltip.tsx +122 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Breadcrumb navigation component
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <Breadcrumb>
|
|
9
|
+
* <BreadcrumbList>
|
|
10
|
+
* <BreadcrumbItem>
|
|
11
|
+
* <BreadcrumbLink href="/">Home</BreadcrumbLink>
|
|
12
|
+
* </BreadcrumbItem>
|
|
13
|
+
* <BreadcrumbSeparator />
|
|
14
|
+
* <BreadcrumbItem>
|
|
15
|
+
* <BreadcrumbPage>Current Page</BreadcrumbPage>
|
|
16
|
+
* </BreadcrumbItem>
|
|
17
|
+
* </BreadcrumbList>
|
|
18
|
+
* </Breadcrumb>
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const Breadcrumb = React.forwardRef<
|
|
22
|
+
HTMLElement,
|
|
23
|
+
React.HTMLAttributes<HTMLElement>
|
|
24
|
+
>(({ ...props }, ref) => (
|
|
25
|
+
<nav ref={ref} aria-label="breadcrumb" {...props} />
|
|
26
|
+
))
|
|
27
|
+
Breadcrumb.displayName = "Breadcrumb"
|
|
28
|
+
|
|
29
|
+
const BreadcrumbList = React.forwardRef<
|
|
30
|
+
HTMLOListElement,
|
|
31
|
+
React.OListHTMLAttributes<HTMLOListElement>
|
|
32
|
+
>(({ className, ...props }, ref) => (
|
|
33
|
+
<ol
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={cn(
|
|
36
|
+
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
))
|
|
42
|
+
BreadcrumbList.displayName = "BreadcrumbList"
|
|
43
|
+
|
|
44
|
+
const BreadcrumbItem = React.forwardRef<
|
|
45
|
+
HTMLLIElement,
|
|
46
|
+
React.LiHTMLAttributes<HTMLLIElement>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<li
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("inline-flex items-center gap-1.5", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
))
|
|
54
|
+
BreadcrumbItem.displayName = "BreadcrumbItem"
|
|
55
|
+
|
|
56
|
+
interface BreadcrumbLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
57
|
+
asChild?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const BreadcrumbLink = React.forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
|
|
61
|
+
({ asChild, className, children, ...props }, ref) => {
|
|
62
|
+
if (asChild && React.isValidElement(children)) {
|
|
63
|
+
return React.cloneElement(children as React.ReactElement<any>, {
|
|
64
|
+
ref,
|
|
65
|
+
className: cn("transition-colors hover:text-foreground", className),
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<a
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={cn("transition-colors hover:text-foreground", className)}
|
|
73
|
+
{...props}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</a>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
BreadcrumbLink.displayName = "BreadcrumbLink"
|
|
81
|
+
|
|
82
|
+
const BreadcrumbPage = React.forwardRef<
|
|
83
|
+
HTMLSpanElement,
|
|
84
|
+
React.HTMLAttributes<HTMLSpanElement>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<span
|
|
87
|
+
ref={ref}
|
|
88
|
+
role="link"
|
|
89
|
+
aria-disabled="true"
|
|
90
|
+
aria-current="page"
|
|
91
|
+
className={cn("font-normal text-foreground", className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
))
|
|
95
|
+
BreadcrumbPage.displayName = "BreadcrumbPage"
|
|
96
|
+
|
|
97
|
+
const BreadcrumbSeparator = ({
|
|
98
|
+
children,
|
|
99
|
+
className,
|
|
100
|
+
...props
|
|
101
|
+
}: React.LiHTMLAttributes<HTMLLIElement>) => (
|
|
102
|
+
<li
|
|
103
|
+
role="presentation"
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
className={cn("[&>svg]:h-3.5 [&>svg]:w-3.5", className)}
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
{children || (
|
|
109
|
+
<svg
|
|
110
|
+
className="h-4 w-4"
|
|
111
|
+
fill="none"
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
strokeWidth={2}
|
|
115
|
+
>
|
|
116
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
117
|
+
</svg>
|
|
118
|
+
)}
|
|
119
|
+
</li>
|
|
120
|
+
)
|
|
121
|
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
|
122
|
+
|
|
123
|
+
const BreadcrumbEllipsis = ({
|
|
124
|
+
className,
|
|
125
|
+
...props
|
|
126
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
127
|
+
<span
|
|
128
|
+
role="presentation"
|
|
129
|
+
aria-hidden="true"
|
|
130
|
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
|
131
|
+
{...props}
|
|
132
|
+
>
|
|
133
|
+
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
|
134
|
+
<circle cx="12" cy="12" r="1" />
|
|
135
|
+
<circle cx="19" cy="12" r="1" />
|
|
136
|
+
<circle cx="5" cy="12" r="1" />
|
|
137
|
+
</svg>
|
|
138
|
+
<span className="sr-only">More</span>
|
|
139
|
+
</span>
|
|
140
|
+
)
|
|
141
|
+
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis"
|
|
142
|
+
|
|
143
|
+
export {
|
|
144
|
+
Breadcrumb,
|
|
145
|
+
BreadcrumbList,
|
|
146
|
+
BreadcrumbItem,
|
|
147
|
+
BreadcrumbLink,
|
|
148
|
+
BreadcrumbPage,
|
|
149
|
+
BreadcrumbSeparator,
|
|
150
|
+
BreadcrumbEllipsis,
|
|
151
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const buttonGroupVariants = cva("inline-flex", {
|
|
6
|
+
variants: {
|
|
7
|
+
orientation: {
|
|
8
|
+
horizontal: "flex-row",
|
|
9
|
+
vertical: "flex-col",
|
|
10
|
+
},
|
|
11
|
+
attached: {
|
|
12
|
+
true: "",
|
|
13
|
+
false: "gap-2",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
compoundVariants: [
|
|
17
|
+
{
|
|
18
|
+
orientation: "horizontal",
|
|
19
|
+
attached: true,
|
|
20
|
+
className:
|
|
21
|
+
"[&>*:not(:first-child):not(:last-child)]:rounded-none [&>*:first-child]:rounded-r-none [&>*:last-child]:rounded-l-none [&>*:not(:first-child)]:-ml-px",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
orientation: "vertical",
|
|
25
|
+
attached: true,
|
|
26
|
+
className:
|
|
27
|
+
"[&>*:not(:first-child):not(:last-child)]:rounded-none [&>*:first-child]:rounded-b-none [&>*:last-child]:rounded-t-none [&>*:not(:first-child)]:-mt-px",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
orientation: "horizontal",
|
|
32
|
+
attached: true,
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
type ButtonGroupVariants = VariantProps<typeof buttonGroupVariants>
|
|
37
|
+
|
|
38
|
+
interface ButtonGroupBaseProps extends ButtonGroupVariants {
|
|
39
|
+
className?: string
|
|
40
|
+
children?: React.ReactNode
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Polymorphic ButtonGroup to group buttons together
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* <ButtonGroup>
|
|
48
|
+
* <Button>Left</Button>
|
|
49
|
+
* <Button>Center</Button>
|
|
50
|
+
* <Button>Right</Button>
|
|
51
|
+
* </ButtonGroup>
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* <ButtonGroup attached={false}>
|
|
55
|
+
* <Button>Spaced</Button>
|
|
56
|
+
* <Button>Buttons</Button>
|
|
57
|
+
* </ButtonGroup>
|
|
58
|
+
*/
|
|
59
|
+
const ButtonGroup = React.forwardRef(
|
|
60
|
+
<T extends React.ElementType = "div">(
|
|
61
|
+
{
|
|
62
|
+
as,
|
|
63
|
+
className,
|
|
64
|
+
orientation,
|
|
65
|
+
attached,
|
|
66
|
+
...props
|
|
67
|
+
}: ButtonGroupBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonGroupBaseProps | "as">,
|
|
68
|
+
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
69
|
+
) => {
|
|
70
|
+
const Comp = as || "div"
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Comp
|
|
74
|
+
ref={ref as any}
|
|
75
|
+
role="group"
|
|
76
|
+
className={cn(buttonGroupVariants({ orientation, attached, className }))}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
ButtonGroup.displayName = "ButtonGroup"
|
|
83
|
+
|
|
84
|
+
export { ButtonGroup, buttonGroupVariants }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default:
|
|
11
|
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
14
|
+
outline:
|
|
15
|
+
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
18
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
19
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
20
|
+
},
|
|
21
|
+
size: {
|
|
22
|
+
default: "h-9 px-4 py-2",
|
|
23
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
24
|
+
lg: "h-10 rounded-md px-8",
|
|
25
|
+
icon: "h-9 w-9",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "default",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
type ButtonVariants = VariantProps<typeof buttonVariants>
|
|
36
|
+
|
|
37
|
+
interface ButtonBaseProps extends ButtonVariants {
|
|
38
|
+
className?: string
|
|
39
|
+
children?: React.ReactNode
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Polymorphic Button component
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // As a button (default)
|
|
47
|
+
* <Button variant="outline">Click me</Button>
|
|
48
|
+
*
|
|
49
|
+
* // As a link
|
|
50
|
+
* <Button as="a" href="/home" variant="link">Go Home</Button>
|
|
51
|
+
*
|
|
52
|
+
* // With loading state
|
|
53
|
+
* <Button disabled>
|
|
54
|
+
* <LoadingSpinner /> Processing...
|
|
55
|
+
* </Button>
|
|
56
|
+
*/
|
|
57
|
+
const Button = React.forwardRef(
|
|
58
|
+
<T extends React.ElementType = "button">(
|
|
59
|
+
{
|
|
60
|
+
as,
|
|
61
|
+
className,
|
|
62
|
+
variant,
|
|
63
|
+
size,
|
|
64
|
+
...props
|
|
65
|
+
}: ButtonBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof ButtonBaseProps | "as">,
|
|
66
|
+
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
67
|
+
) => {
|
|
68
|
+
const Comp = as || "button"
|
|
69
|
+
|
|
70
|
+
// Ensure proper keyboard handling for non-button elements
|
|
71
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
72
|
+
if (Comp !== "button" && (e.key === "Enter" || e.key === " ")) {
|
|
73
|
+
e.preventDefault()
|
|
74
|
+
; (e.currentTarget as HTMLElement).click()
|
|
75
|
+
}
|
|
76
|
+
// Call original onKeyDown if provided
|
|
77
|
+
const originalOnKeyDown = (props as any).onKeyDown
|
|
78
|
+
if (originalOnKeyDown) {
|
|
79
|
+
originalOnKeyDown(e)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add role="button" for non-button elements
|
|
84
|
+
const accessibilityProps = Comp !== "button" ? {
|
|
85
|
+
role: "button",
|
|
86
|
+
tabIndex: 0,
|
|
87
|
+
onKeyDown: handleKeyDown,
|
|
88
|
+
} : {}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Comp
|
|
92
|
+
ref={ref as any}
|
|
93
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
94
|
+
{...accessibilityProps}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
Button.displayName = "Button"
|
|
101
|
+
|
|
102
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
interface CalendarContextValue {
|
|
5
|
+
currentMonth: Date
|
|
6
|
+
setCurrentMonth: (date: Date) => void
|
|
7
|
+
selectedDates: Date[]
|
|
8
|
+
onSelect: (date: Date) => void
|
|
9
|
+
mode: "single" | "multiple" | "range"
|
|
10
|
+
rangeStart: Date | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CalendarContext = React.createContext<CalendarContextValue | null>(null)
|
|
14
|
+
|
|
15
|
+
interface CalendarProps {
|
|
16
|
+
/** Selection mode */
|
|
17
|
+
mode?: "single" | "multiple" | "range"
|
|
18
|
+
/** Selected date(s) */
|
|
19
|
+
selected?: Date | Date[]
|
|
20
|
+
/** Callback when date is selected */
|
|
21
|
+
onSelect?: (date: Date | Date[] | undefined) => void
|
|
22
|
+
/** Default month to display */
|
|
23
|
+
defaultMonth?: Date
|
|
24
|
+
/** Minimum selectable date */
|
|
25
|
+
minDate?: Date
|
|
26
|
+
/** Maximum selectable date */
|
|
27
|
+
maxDate?: Date
|
|
28
|
+
/** Whether calendar is disabled */
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
className?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
|
|
34
|
+
const MONTHS = [
|
|
35
|
+
"January", "February", "March", "April", "May", "June",
|
|
36
|
+
"July", "August", "September", "October", "November", "December"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
function getDaysInMonth(year: number, month: number): Date[] {
|
|
40
|
+
const days: Date[] = []
|
|
41
|
+
const firstDay = new Date(year, month, 1)
|
|
42
|
+
const lastDay = new Date(year, month + 1, 0)
|
|
43
|
+
|
|
44
|
+
// Add days from previous month to fill first week
|
|
45
|
+
const startPadding = firstDay.getDay()
|
|
46
|
+
for (let i = startPadding - 1; i >= 0; i--) {
|
|
47
|
+
days.push(new Date(year, month, -i))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add days of current month
|
|
51
|
+
for (let d = 1; d <= lastDay.getDate(); d++) {
|
|
52
|
+
days.push(new Date(year, month, d))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add days from next month to fill last week
|
|
56
|
+
const endPadding = 42 - days.length // 6 rows * 7 days
|
|
57
|
+
for (let i = 1; i <= endPadding; i++) {
|
|
58
|
+
days.push(new Date(year, month + 1, i))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return days
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isSameDay(d1: Date, d2: Date): boolean {
|
|
65
|
+
return d1.getDate() === d2.getDate() &&
|
|
66
|
+
d1.getMonth() === d2.getMonth() &&
|
|
67
|
+
d1.getFullYear() === d2.getFullYear()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isInRange(date: Date, start: Date | null, end: Date | null): boolean {
|
|
71
|
+
if (!start || !end) return false
|
|
72
|
+
const d = date.getTime()
|
|
73
|
+
return d >= start.getTime() && d <= end.getTime()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calendar date picker component
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // Single date
|
|
81
|
+
* const [date, setDate] = useState<Date>()
|
|
82
|
+
* <Calendar mode="single" selected={date} onSelect={setDate} />
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Date range
|
|
86
|
+
* const [range, setRange] = useState<Date[]>([])
|
|
87
|
+
* <Calendar mode="range" selected={range} onSelect={setRange} />
|
|
88
|
+
*/
|
|
89
|
+
const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
|
|
90
|
+
(
|
|
91
|
+
{
|
|
92
|
+
mode = "single",
|
|
93
|
+
selected,
|
|
94
|
+
onSelect,
|
|
95
|
+
defaultMonth = new Date(),
|
|
96
|
+
minDate,
|
|
97
|
+
maxDate,
|
|
98
|
+
disabled,
|
|
99
|
+
className,
|
|
100
|
+
},
|
|
101
|
+
ref
|
|
102
|
+
) => {
|
|
103
|
+
const [currentMonth, setCurrentMonth] = React.useState(defaultMonth)
|
|
104
|
+
const [rangeStart, setRangeStart] = React.useState<Date | null>(null)
|
|
105
|
+
|
|
106
|
+
const selectedDates = React.useMemo(() => {
|
|
107
|
+
if (!selected) return []
|
|
108
|
+
return Array.isArray(selected) ? selected : [selected]
|
|
109
|
+
}, [selected])
|
|
110
|
+
|
|
111
|
+
const handleSelect = (date: Date) => {
|
|
112
|
+
if (disabled) return
|
|
113
|
+
if (minDate && date < minDate) return
|
|
114
|
+
if (maxDate && date > maxDate) return
|
|
115
|
+
|
|
116
|
+
if (mode === "single") {
|
|
117
|
+
onSelect?.(date)
|
|
118
|
+
} else if (mode === "multiple") {
|
|
119
|
+
const exists = selectedDates.some(d => isSameDay(d, date))
|
|
120
|
+
if (exists) {
|
|
121
|
+
onSelect?.(selectedDates.filter(d => !isSameDay(d, date)))
|
|
122
|
+
} else {
|
|
123
|
+
onSelect?.([...selectedDates, date])
|
|
124
|
+
}
|
|
125
|
+
} else if (mode === "range") {
|
|
126
|
+
if (!rangeStart) {
|
|
127
|
+
setRangeStart(date)
|
|
128
|
+
onSelect?.([date])
|
|
129
|
+
} else {
|
|
130
|
+
const start = rangeStart < date ? rangeStart : date
|
|
131
|
+
const end = rangeStart < date ? date : rangeStart
|
|
132
|
+
onSelect?.([start, end])
|
|
133
|
+
setRangeStart(null)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const navigatePrev = () => {
|
|
139
|
+
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const navigateNext = () => {
|
|
143
|
+
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const days = getDaysInMonth(currentMonth.getFullYear(), currentMonth.getMonth())
|
|
147
|
+
const rangeEnd = mode === "range" && selectedDates.length === 2 ? selectedDates[1] : null
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
ref={ref}
|
|
152
|
+
className={cn("p-3 bg-background border rounded-lg shadow-md w-fit", className)}
|
|
153
|
+
role="application"
|
|
154
|
+
aria-label="Calendar"
|
|
155
|
+
>
|
|
156
|
+
{/* Header */}
|
|
157
|
+
<div className="flex items-center justify-between mb-4">
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={navigatePrev}
|
|
161
|
+
className="p-1 rounded hover:bg-accent"
|
|
162
|
+
aria-label="Previous month"
|
|
163
|
+
>
|
|
164
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
165
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
166
|
+
</svg>
|
|
167
|
+
</button>
|
|
168
|
+
<span className="font-semibold">
|
|
169
|
+
{MONTHS[currentMonth.getMonth()]} {currentMonth.getFullYear()}
|
|
170
|
+
</span>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
onClick={navigateNext}
|
|
174
|
+
className="p-1 rounded hover:bg-accent"
|
|
175
|
+
aria-label="Next month"
|
|
176
|
+
>
|
|
177
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
178
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
179
|
+
</svg>
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* Day names */}
|
|
184
|
+
<div className="grid grid-cols-7 gap-1 mb-1">
|
|
185
|
+
{DAYS.map(day => (
|
|
186
|
+
<div key={day} className="text-center text-xs text-muted-foreground font-medium py-1">
|
|
187
|
+
{day}
|
|
188
|
+
</div>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Days grid */}
|
|
193
|
+
<div className="grid grid-cols-7 gap-1" role="grid">
|
|
194
|
+
{days.map((date, index) => {
|
|
195
|
+
const isCurrentMonth = date.getMonth() === currentMonth.getMonth()
|
|
196
|
+
const isSelected = selectedDates.some(d => isSameDay(d, date))
|
|
197
|
+
const isRangeStart = rangeStart && isSameDay(date, rangeStart)
|
|
198
|
+
const isRangeEnd = rangeEnd && isSameDay(date, rangeEnd)
|
|
199
|
+
const inRange = isInRange(date, rangeStart || selectedDates[0], rangeEnd)
|
|
200
|
+
const isToday = isSameDay(date, new Date())
|
|
201
|
+
const isDisabled = disabled ||
|
|
202
|
+
(minDate && date < minDate) ||
|
|
203
|
+
(maxDate && date > maxDate)
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<button
|
|
207
|
+
key={index}
|
|
208
|
+
type="button"
|
|
209
|
+
role="gridcell"
|
|
210
|
+
aria-selected={isSelected}
|
|
211
|
+
aria-disabled={isDisabled}
|
|
212
|
+
disabled={isDisabled}
|
|
213
|
+
onClick={() => handleSelect(date)}
|
|
214
|
+
className={cn(
|
|
215
|
+
"h-8 w-8 rounded text-sm font-medium transition-colors",
|
|
216
|
+
"focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1",
|
|
217
|
+
!isCurrentMonth && "text-muted-foreground/50",
|
|
218
|
+
isCurrentMonth && "text-foreground",
|
|
219
|
+
isToday && "border border-primary",
|
|
220
|
+
isSelected && "bg-primary text-primary-foreground",
|
|
221
|
+
(isRangeStart || isRangeEnd) && "bg-primary text-primary-foreground",
|
|
222
|
+
inRange && !isSelected && "bg-accent",
|
|
223
|
+
!isSelected && !inRange && "hover:bg-accent",
|
|
224
|
+
isDisabled && "opacity-50 cursor-not-allowed"
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{date.getDate()}
|
|
228
|
+
</button>
|
|
229
|
+
)
|
|
230
|
+
})}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
Calendar.displayName = "Calendar"
|
|
237
|
+
|
|
238
|
+
export { Calendar }
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@/lib/utils"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Card component - Container with header, content, and footer
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <Card>
|
|
9
|
+
* <CardHeader>
|
|
10
|
+
* <CardTitle>Title</CardTitle>
|
|
11
|
+
* <CardDescription>Description</CardDescription>
|
|
12
|
+
* </CardHeader>
|
|
13
|
+
* <CardContent>Content goes here</CardContent>
|
|
14
|
+
* <CardFooter>Footer actions</CardFooter>
|
|
15
|
+
* </Card>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface CardBaseProps {
|
|
19
|
+
className?: string
|
|
20
|
+
children?: React.ReactNode
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const Card = React.forwardRef(
|
|
24
|
+
<T extends React.ElementType = "div">(
|
|
25
|
+
{
|
|
26
|
+
as,
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: CardBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof CardBaseProps | "as">,
|
|
30
|
+
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
31
|
+
) => {
|
|
32
|
+
const Comp = as || "div"
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Comp
|
|
36
|
+
ref={ref as any}
|
|
37
|
+
className={cn(
|
|
38
|
+
"rounded-xl border bg-card text-card-foreground shadow",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
Card.displayName = "Card"
|
|
47
|
+
|
|
48
|
+
const CardHeader = React.forwardRef<
|
|
49
|
+
HTMLDivElement,
|
|
50
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
51
|
+
>(({ className, ...props }, ref) => (
|
|
52
|
+
<div
|
|
53
|
+
ref={ref}
|
|
54
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
))
|
|
58
|
+
CardHeader.displayName = "CardHeader"
|
|
59
|
+
|
|
60
|
+
const CardTitle = React.forwardRef(
|
|
61
|
+
<T extends React.ElementType = "h3">(
|
|
62
|
+
{
|
|
63
|
+
as,
|
|
64
|
+
className,
|
|
65
|
+
...props
|
|
66
|
+
}: CardBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof CardBaseProps | "as">,
|
|
67
|
+
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
68
|
+
) => {
|
|
69
|
+
const Comp = as || "h3"
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Comp
|
|
73
|
+
ref={ref as any}
|
|
74
|
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
CardTitle.displayName = "CardTitle"
|
|
81
|
+
|
|
82
|
+
const CardDescription = React.forwardRef<
|
|
83
|
+
HTMLParagraphElement,
|
|
84
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<p
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
))
|
|
92
|
+
CardDescription.displayName = "CardDescription"
|
|
93
|
+
|
|
94
|
+
const CardContent = React.forwardRef<
|
|
95
|
+
HTMLDivElement,
|
|
96
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
97
|
+
>(({ className, ...props }, ref) => (
|
|
98
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
99
|
+
))
|
|
100
|
+
CardContent.displayName = "CardContent"
|
|
101
|
+
|
|
102
|
+
const CardFooter = React.forwardRef<
|
|
103
|
+
HTMLDivElement,
|
|
104
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
105
|
+
>(({ className, ...props }, ref) => (
|
|
106
|
+
<div
|
|
107
|
+
ref={ref}
|
|
108
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
))
|
|
112
|
+
CardFooter.displayName = "CardFooter"
|
|
113
|
+
|
|
114
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|