@srcroot/ui 0.0.54 → 0.0.56
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 -151
- package/dist/index.d.ts +0 -0
- package/dist/index.js +55 -1
- package/package.json +7 -2
- package/src/registry/analytics/google-analytics.tsx +36 -39
- package/src/registry/analytics/google-tag-manager.tsx +62 -65
- package/src/registry/analytics/meta-pixel.tsx +44 -47
- package/src/registry/analytics/microsoft-clarity.tsx +31 -34
- package/src/registry/analytics/tiktok-pixel.tsx +34 -37
- package/src/registry/lib/utils.ts +0 -0
- package/src/registry/themes/v3/blue.css +157 -157
- package/src/registry/themes/v3/glass.css +153 -153
- package/src/registry/themes/v3/gray.css +157 -157
- package/src/registry/themes/v3/green.css +157 -157
- package/src/registry/themes/v3/neutral.css +157 -157
- package/src/registry/themes/v3/orange.css +157 -157
- package/src/registry/themes/v3/rose.css +157 -157
- package/src/registry/themes/v3/slate.css +157 -157
- package/src/registry/themes/v3/stone.css +157 -157
- package/src/registry/themes/v3/violet.css +186 -186
- package/src/registry/themes/v3/zinc.css +157 -157
- package/src/registry/themes/v4/blue.css +184 -184
- package/src/registry/themes/v4/glass.css +180 -180
- package/src/registry/themes/v4/gray.css +184 -184
- package/src/registry/themes/v4/green.css +184 -184
- package/src/registry/themes/v4/neutral.css +184 -184
- package/src/registry/themes/v4/orange.css +184 -184
- package/src/registry/themes/v4/rose.css +184 -184
- package/src/registry/themes/v4/slate.css +184 -184
- package/src/registry/themes/v4/stone.css +184 -184
- package/src/registry/themes/v4/violet.css +184 -184
- package/src/registry/themes/v4/zinc.css +184 -184
- package/src/registry/ui/accordion.tsx +164 -165
- package/src/registry/ui/alert-dialog.tsx +213 -214
- package/src/registry/ui/alert.tsx +73 -76
- package/src/registry/ui/aspect-ratio.tsx +44 -47
- package/src/registry/ui/avatar.tsx +96 -97
- package/src/registry/ui/badge.tsx +52 -55
- package/src/registry/ui/breadcrumb.tsx +147 -150
- package/src/registry/ui/button-group.tsx +64 -67
- package/src/registry/ui/button.tsx +71 -72
- package/src/registry/ui/calendar.tsx +514 -515
- package/src/registry/ui/card.tsx +88 -91
- package/src/registry/ui/carousel.tsx +214 -214
- package/src/registry/ui/chart.tsx +373 -373
- package/src/registry/ui/chatbot.tsx +86 -13
- package/src/registry/ui/checkbox.tsx +93 -94
- package/src/registry/ui/collapsible.tsx +107 -108
- package/src/registry/ui/combobox.tsx +171 -171
- package/src/registry/ui/command.tsx +300 -300
- package/src/registry/ui/container.tsx +44 -47
- package/src/registry/ui/context-menu.tsx +221 -221
- package/src/registry/ui/date-picker.tsx +228 -228
- package/src/registry/ui/dialog.tsx +269 -270
- package/src/registry/ui/drawer.tsx +10 -4
- package/src/registry/ui/dropdown-menu.tsx +529 -530
- package/src/registry/ui/empty-state.tsx +0 -2
- package/src/registry/ui/file-upload.tsx +0 -0
- package/src/registry/ui/floating-dock.tsx +0 -0
- package/src/registry/ui/form-field.tsx +91 -94
- package/src/registry/ui/google-analytics.tsx +38 -0
- package/src/registry/ui/google-tag-manager.tsx +64 -0
- package/src/registry/ui/hover-card.tsx +223 -223
- package/src/registry/ui/image.tsx +144 -147
- package/src/registry/ui/input-group.tsx +82 -85
- package/src/registry/ui/input.tsx +125 -125
- package/src/registry/ui/kbd.tsx +60 -63
- package/src/registry/ui/label.tsx +36 -37
- package/src/registry/ui/loading-spinner.tsx +108 -111
- package/src/registry/ui/map.tsx +0 -0
- package/src/registry/ui/marquee.tsx +2 -0
- package/src/registry/ui/menubar.tsx +246 -246
- package/src/registry/ui/meta-pixel.tsx +46 -0
- package/src/registry/ui/microsoft-clarity.tsx +33 -0
- package/src/registry/ui/native-select.tsx +49 -52
- package/src/registry/ui/otp-input.tsx +152 -155
- package/src/registry/ui/pagination.tsx +149 -152
- package/src/registry/ui/patterns.tsx +28 -0
- package/src/registry/ui/popover.tsx +226 -227
- package/src/registry/ui/progress.tsx +51 -52
- package/src/registry/ui/radio.tsx +99 -102
- package/src/registry/ui/resizable.tsx +314 -314
- package/src/registry/ui/scroll-animation.tsx +45 -0
- package/src/registry/ui/scroll-area.tsx +121 -122
- package/src/registry/ui/scroll-to-top.tsx +0 -0
- package/src/registry/ui/search.tsx +147 -150
- package/src/registry/ui/select.tsx +292 -293
- package/src/registry/ui/separator.tsx +46 -47
- package/src/registry/ui/sheet.tsx +6 -3
- package/src/registry/ui/sidebar.tsx +628 -628
- package/src/registry/ui/skeleton.tsx +26 -29
- package/src/registry/ui/slider.tsx +196 -197
- package/src/registry/ui/slot.tsx +69 -72
- package/src/registry/ui/star-rating.tsx +131 -134
- package/src/registry/ui/switch.tsx +72 -73
- package/src/registry/ui/table-of-contents.tsx +96 -96
- package/src/registry/ui/table.tsx +138 -139
- package/src/registry/ui/tabs.tsx +124 -125
- package/src/registry/ui/text.tsx +61 -64
- package/src/registry/ui/textarea.tsx +41 -42
- package/src/registry/ui/theme-switcher.tsx +66 -66
- package/src/registry/ui/tiktok-pixel.tsx +36 -0
- package/src/registry/ui/toast.tsx +97 -98
- package/src/registry/ui/toggle-group.tsx +129 -129
- package/src/registry/ui/toggle.tsx +72 -72
- package/src/registry/ui/tooltip.tsx +143 -144
- package/src/registry/ui/whatsapp.tsx +0 -0
package/src/registry/ui/card.tsx
CHANGED
|
@@ -1,91 +1,88 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* </
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
CardFooter
|
|
89
|
-
|
|
90
|
-
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
91
|
-
|
|
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
|
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
19
|
+
({ className, ...props }, ref) => (
|
|
20
|
+
<div
|
|
21
|
+
ref={ref}
|
|
22
|
+
className={cn(
|
|
23
|
+
"rounded-xl border bg-card text-card-foreground shadow",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
Card.displayName = "Card"
|
|
31
|
+
|
|
32
|
+
const CardHeader = React.forwardRef<
|
|
33
|
+
HTMLDivElement,
|
|
34
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<div
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
))
|
|
42
|
+
CardHeader.displayName = "CardHeader"
|
|
43
|
+
|
|
44
|
+
const CardTitle = React.forwardRef<
|
|
45
|
+
HTMLHeadingElement,
|
|
46
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<h3
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
))
|
|
54
|
+
CardTitle.displayName = "CardTitle"
|
|
55
|
+
|
|
56
|
+
const CardDescription = React.forwardRef<
|
|
57
|
+
HTMLParagraphElement,
|
|
58
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
59
|
+
>(({ className, ...props }, ref) => (
|
|
60
|
+
<p
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
))
|
|
66
|
+
CardDescription.displayName = "CardDescription"
|
|
67
|
+
|
|
68
|
+
const CardContent = React.forwardRef<
|
|
69
|
+
HTMLDivElement,
|
|
70
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
71
|
+
>(({ className, ...props }, ref) => (
|
|
72
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
73
|
+
))
|
|
74
|
+
CardContent.displayName = "CardContent"
|
|
75
|
+
|
|
76
|
+
const CardFooter = React.forwardRef<
|
|
77
|
+
HTMLDivElement,
|
|
78
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
79
|
+
>(({ className, ...props }, ref) => (
|
|
80
|
+
<div
|
|
81
|
+
ref={ref}
|
|
82
|
+
className={cn("flex items-center p-6 pt-0", className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
))
|
|
86
|
+
CardFooter.displayName = "CardFooter"
|
|
87
|
+
|
|
88
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
|
@@ -1,214 +1,214 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
import * as React from "react"
|
|
3
|
-
import { cn } from "@/lib/utils"
|
|
4
|
-
|
|
5
|
-
interface CarouselContextValue {
|
|
6
|
-
currentIndex: number
|
|
7
|
-
setCurrentIndex: React.Dispatch<React.SetStateAction<number>>
|
|
8
|
-
itemsCount: number
|
|
9
|
-
setItemsCount: React.Dispatch<React.SetStateAction<number>>
|
|
10
|
-
isTransitioning: boolean
|
|
11
|
-
setIsTransitioning: React.Dispatch<React.SetStateAction<boolean>>
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const CarouselContext = React.createContext<CarouselContextValue | null>(null)
|
|
15
|
-
|
|
16
|
-
interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
17
|
-
/** Auto-play interval in ms (0 to disable) */
|
|
18
|
-
autoPlay?: number
|
|
19
|
-
/** Loop back to start (ignored in this infinite implementation as it's always true-ish, but kept for API) */
|
|
20
|
-
loop?: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Carousel/Slider component with Infinite Looping
|
|
25
|
-
*/
|
|
26
|
-
const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
|
|
27
|
-
({ className, children, autoPlay = 0, loop = true, ...props }, ref) => {
|
|
28
|
-
// Start at 1 because 0 is the clone of the last item
|
|
29
|
-
const [currentIndex, setCurrentIndex] = React.useState(1)
|
|
30
|
-
const [itemsCount, setItemsCount] = React.useState(0)
|
|
31
|
-
const [isTransitioning, setIsTransitioning] = React.useState(true)
|
|
32
|
-
const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
33
|
-
|
|
34
|
-
React.useEffect(() => {
|
|
35
|
-
if (autoPlay > 0 && itemsCount > 1) {
|
|
36
|
-
intervalRef.current = setInterval(() => {
|
|
37
|
-
setIsTransitioning(true)
|
|
38
|
-
setCurrentIndex((prev) => prev + 1)
|
|
39
|
-
}, autoPlay)
|
|
40
|
-
return () => {
|
|
41
|
-
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}, [autoPlay, itemsCount])
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<CarouselContext.Provider value={{
|
|
48
|
-
currentIndex,
|
|
49
|
-
setCurrentIndex,
|
|
50
|
-
itemsCount,
|
|
51
|
-
setItemsCount,
|
|
52
|
-
isTransitioning,
|
|
53
|
-
setIsTransitioning
|
|
54
|
-
}}>
|
|
55
|
-
<div
|
|
56
|
-
ref={ref}
|
|
57
|
-
className={cn("relative", className)}
|
|
58
|
-
role="region"
|
|
59
|
-
aria-roledescription="carousel"
|
|
60
|
-
{...props}
|
|
61
|
-
>
|
|
62
|
-
{children}
|
|
63
|
-
</div>
|
|
64
|
-
</CarouselContext.Provider>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
)
|
|
68
|
-
Carousel.displayName = "Carousel"
|
|
69
|
-
|
|
70
|
-
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
71
|
-
({ className, children, ...props }, ref) => {
|
|
72
|
-
const context = React.useContext(CarouselContext)
|
|
73
|
-
if (!context) throw new Error("CarouselContent must be used within Carousel")
|
|
74
|
-
|
|
75
|
-
const items = React.Children.toArray(children)
|
|
76
|
-
|
|
77
|
-
React.useEffect(() => {
|
|
78
|
-
context.setItemsCount(items.length)
|
|
79
|
-
}, [items.length, context])
|
|
80
|
-
|
|
81
|
-
// Clone first and last items for infinite loop illusion
|
|
82
|
-
const firstClone = items.length > 0 && React.isValidElement(items[0])
|
|
83
|
-
? React.cloneElement(items[0] as React.ReactElement, { key: "clone-first" })
|
|
84
|
-
: null
|
|
85
|
-
const lastClone = items.length > 0 && React.isValidElement(items[items.length - 1])
|
|
86
|
-
? React.cloneElement(items[items.length - 1] as React.ReactElement, { key: "clone-last" })
|
|
87
|
-
: null
|
|
88
|
-
|
|
89
|
-
// If we have items, prepend last-clone and append first-clone
|
|
90
|
-
const displayItems = items.length > 1 ? [lastClone, ...items, firstClone] : items
|
|
91
|
-
|
|
92
|
-
const handleTransitionEnd = () => {
|
|
93
|
-
if (items.length <= 1) return
|
|
94
|
-
|
|
95
|
-
// If reached the end clone (index = N + 1), snap back to first real item (index = 1)
|
|
96
|
-
if (context.currentIndex >= items.length + 1) {
|
|
97
|
-
context.setIsTransitioning(false)
|
|
98
|
-
context.setCurrentIndex(1)
|
|
99
|
-
}
|
|
100
|
-
// If reached the start clone (index = 0), snap forward to last real item (index = N)
|
|
101
|
-
else if (context.currentIndex <= 0) {
|
|
102
|
-
context.setIsTransitioning(false)
|
|
103
|
-
context.setCurrentIndex(items.length)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Re-enable transition after a snap (on next frame)
|
|
108
|
-
React.useEffect(() => {
|
|
109
|
-
if (!context.isTransitioning) {
|
|
110
|
-
const timer = setTimeout(() => {
|
|
111
|
-
context.setIsTransitioning(true)
|
|
112
|
-
}, 50)
|
|
113
|
-
return () => clearTimeout(timer)
|
|
114
|
-
}
|
|
115
|
-
}, [context.isTransitioning, context])
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div ref={ref} className={cn("overflow-hidden", className)} {...props}>
|
|
119
|
-
<div
|
|
120
|
-
className="flex h-full w-full"
|
|
121
|
-
style={{
|
|
122
|
-
transform: `translateX(-${context.currentIndex * 100}%)`,
|
|
123
|
-
transition: context.isTransitioning ? 'transform 300ms ease-in-out' : 'none'
|
|
124
|
-
}}
|
|
125
|
-
onTransitionEnd={handleTransitionEnd}
|
|
126
|
-
>
|
|
127
|
-
{displayItems}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
)
|
|
133
|
-
CarouselContent.displayName = "CarouselContent"
|
|
134
|
-
|
|
135
|
-
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
136
|
-
({ className, ...props }, ref) => (
|
|
137
|
-
<div
|
|
138
|
-
ref={ref}
|
|
139
|
-
role="group"
|
|
140
|
-
aria-roledescription="slide"
|
|
141
|
-
className={cn("min-w-0 h-full shrink-0 grow-0 basis-full", className)}
|
|
142
|
-
{...props}
|
|
143
|
-
/>
|
|
144
|
-
)
|
|
145
|
-
)
|
|
146
|
-
CarouselItem.displayName = "CarouselItem"
|
|
147
|
-
|
|
148
|
-
const CarouselPrevious = React.forwardRef<
|
|
149
|
-
HTMLButtonElement,
|
|
150
|
-
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
151
|
-
>(({ className, ...props }, ref) => {
|
|
152
|
-
const context = React.useContext(CarouselContext)
|
|
153
|
-
if (!context) throw new Error("CarouselPrevious must be used within Carousel")
|
|
154
|
-
|
|
155
|
-
const handlePrev = () => {
|
|
156
|
-
context.setIsTransitioning(true)
|
|
157
|
-
context.setCurrentIndex((prev) => prev - 1)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return (
|
|
161
|
-
<button
|
|
162
|
-
ref={ref}
|
|
163
|
-
type="button"
|
|
164
|
-
className={cn(
|
|
165
|
-
"absolute left-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
|
|
166
|
-
"hover:bg-accent disabled:opacity-50",
|
|
167
|
-
className
|
|
168
|
-
)}
|
|
169
|
-
onClick={handlePrev}
|
|
170
|
-
aria-label="Previous slide"
|
|
171
|
-
{...props}
|
|
172
|
-
>
|
|
173
|
-
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
174
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
175
|
-
</svg>
|
|
176
|
-
</button>
|
|
177
|
-
)
|
|
178
|
-
})
|
|
179
|
-
CarouselPrevious.displayName = "CarouselPrevious"
|
|
180
|
-
|
|
181
|
-
const CarouselNext = React.forwardRef<
|
|
182
|
-
HTMLButtonElement,
|
|
183
|
-
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
184
|
-
>(({ className, ...props }, ref) => {
|
|
185
|
-
const context = React.useContext(CarouselContext)
|
|
186
|
-
if (!context) throw new Error("CarouselNext must be used within Carousel")
|
|
187
|
-
|
|
188
|
-
const handleNext = () => {
|
|
189
|
-
context.setIsTransitioning(true)
|
|
190
|
-
context.setCurrentIndex((prev) => prev + 1)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return (
|
|
194
|
-
<button
|
|
195
|
-
ref={ref}
|
|
196
|
-
type="button"
|
|
197
|
-
className={cn(
|
|
198
|
-
"absolute right-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
|
|
199
|
-
"hover:bg-accent disabled:opacity-50",
|
|
200
|
-
className
|
|
201
|
-
)}
|
|
202
|
-
onClick={handleNext}
|
|
203
|
-
aria-label="Next slide"
|
|
204
|
-
{...props}
|
|
205
|
-
>
|
|
206
|
-
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
207
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
208
|
-
</svg>
|
|
209
|
-
</button>
|
|
210
|
-
)
|
|
211
|
-
})
|
|
212
|
-
CarouselNext.displayName = "CarouselNext"
|
|
213
|
-
|
|
214
|
-
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }
|
|
1
|
+
'use client'
|
|
2
|
+
import * as React from "react"
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
interface CarouselContextValue {
|
|
6
|
+
currentIndex: number
|
|
7
|
+
setCurrentIndex: React.Dispatch<React.SetStateAction<number>>
|
|
8
|
+
itemsCount: number
|
|
9
|
+
setItemsCount: React.Dispatch<React.SetStateAction<number>>
|
|
10
|
+
isTransitioning: boolean
|
|
11
|
+
setIsTransitioning: React.Dispatch<React.SetStateAction<boolean>>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const CarouselContext = React.createContext<CarouselContextValue | null>(null)
|
|
15
|
+
|
|
16
|
+
interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
17
|
+
/** Auto-play interval in ms (0 to disable) */
|
|
18
|
+
autoPlay?: number
|
|
19
|
+
/** Loop back to start (ignored in this infinite implementation as it's always true-ish, but kept for API) */
|
|
20
|
+
loop?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Carousel/Slider component with Infinite Looping
|
|
25
|
+
*/
|
|
26
|
+
const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
|
|
27
|
+
({ className, children, autoPlay = 0, loop = true, ...props }, ref) => {
|
|
28
|
+
// Start at 1 because 0 is the clone of the last item
|
|
29
|
+
const [currentIndex, setCurrentIndex] = React.useState(1)
|
|
30
|
+
const [itemsCount, setItemsCount] = React.useState(0)
|
|
31
|
+
const [isTransitioning, setIsTransitioning] = React.useState(true)
|
|
32
|
+
const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
33
|
+
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (autoPlay > 0 && itemsCount > 1) {
|
|
36
|
+
intervalRef.current = setInterval(() => {
|
|
37
|
+
setIsTransitioning(true)
|
|
38
|
+
setCurrentIndex((prev) => prev + 1)
|
|
39
|
+
}, autoPlay)
|
|
40
|
+
return () => {
|
|
41
|
+
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, [autoPlay, itemsCount])
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<CarouselContext.Provider value={{
|
|
48
|
+
currentIndex,
|
|
49
|
+
setCurrentIndex,
|
|
50
|
+
itemsCount,
|
|
51
|
+
setItemsCount,
|
|
52
|
+
isTransitioning,
|
|
53
|
+
setIsTransitioning
|
|
54
|
+
}}>
|
|
55
|
+
<div
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn("relative", className)}
|
|
58
|
+
role="region"
|
|
59
|
+
aria-roledescription="carousel"
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</div>
|
|
64
|
+
</CarouselContext.Provider>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
Carousel.displayName = "Carousel"
|
|
69
|
+
|
|
70
|
+
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
71
|
+
({ className, children, ...props }, ref) => {
|
|
72
|
+
const context = React.useContext(CarouselContext)
|
|
73
|
+
if (!context) throw new Error("CarouselContent must be used within Carousel")
|
|
74
|
+
|
|
75
|
+
const items = React.Children.toArray(children)
|
|
76
|
+
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
context.setItemsCount(items.length)
|
|
79
|
+
}, [items.length, context])
|
|
80
|
+
|
|
81
|
+
// Clone first and last items for infinite loop illusion
|
|
82
|
+
const firstClone = items.length > 0 && React.isValidElement(items[0])
|
|
83
|
+
? React.cloneElement(items[0] as React.ReactElement, { key: "clone-first" })
|
|
84
|
+
: null
|
|
85
|
+
const lastClone = items.length > 0 && React.isValidElement(items[items.length - 1])
|
|
86
|
+
? React.cloneElement(items[items.length - 1] as React.ReactElement, { key: "clone-last" })
|
|
87
|
+
: null
|
|
88
|
+
|
|
89
|
+
// If we have items, prepend last-clone and append first-clone
|
|
90
|
+
const displayItems = items.length > 1 ? [lastClone, ...items, firstClone] : items
|
|
91
|
+
|
|
92
|
+
const handleTransitionEnd = () => {
|
|
93
|
+
if (items.length <= 1) return
|
|
94
|
+
|
|
95
|
+
// If reached the end clone (index = N + 1), snap back to first real item (index = 1)
|
|
96
|
+
if (context.currentIndex >= items.length + 1) {
|
|
97
|
+
context.setIsTransitioning(false)
|
|
98
|
+
context.setCurrentIndex(1)
|
|
99
|
+
}
|
|
100
|
+
// If reached the start clone (index = 0), snap forward to last real item (index = N)
|
|
101
|
+
else if (context.currentIndex <= 0) {
|
|
102
|
+
context.setIsTransitioning(false)
|
|
103
|
+
context.setCurrentIndex(items.length)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Re-enable transition after a snap (on next frame)
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
if (!context.isTransitioning) {
|
|
110
|
+
const timer = setTimeout(() => {
|
|
111
|
+
context.setIsTransitioning(true)
|
|
112
|
+
}, 50)
|
|
113
|
+
return () => clearTimeout(timer)
|
|
114
|
+
}
|
|
115
|
+
}, [context.isTransitioning, context])
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div ref={ref} className={cn("overflow-hidden", className)} {...props}>
|
|
119
|
+
<div
|
|
120
|
+
className="flex h-full w-full"
|
|
121
|
+
style={{
|
|
122
|
+
transform: `translateX(-${context.currentIndex * 100}%)`,
|
|
123
|
+
transition: context.isTransitioning ? 'transform 300ms ease-in-out' : 'none'
|
|
124
|
+
}}
|
|
125
|
+
onTransitionEnd={handleTransitionEnd}
|
|
126
|
+
>
|
|
127
|
+
{displayItems}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
CarouselContent.displayName = "CarouselContent"
|
|
134
|
+
|
|
135
|
+
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
136
|
+
({ className, ...props }, ref) => (
|
|
137
|
+
<div
|
|
138
|
+
ref={ref}
|
|
139
|
+
role="group"
|
|
140
|
+
aria-roledescription="slide"
|
|
141
|
+
className={cn("min-w-0 h-full shrink-0 grow-0 basis-full", className)}
|
|
142
|
+
{...props}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
CarouselItem.displayName = "CarouselItem"
|
|
147
|
+
|
|
148
|
+
const CarouselPrevious = React.forwardRef<
|
|
149
|
+
HTMLButtonElement,
|
|
150
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
151
|
+
>(({ className, ...props }, ref) => {
|
|
152
|
+
const context = React.useContext(CarouselContext)
|
|
153
|
+
if (!context) throw new Error("CarouselPrevious must be used within Carousel")
|
|
154
|
+
|
|
155
|
+
const handlePrev = () => {
|
|
156
|
+
context.setIsTransitioning(true)
|
|
157
|
+
context.setCurrentIndex((prev) => prev - 1)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<button
|
|
162
|
+
ref={ref}
|
|
163
|
+
type="button"
|
|
164
|
+
className={cn(
|
|
165
|
+
"absolute left-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
|
|
166
|
+
"hover:bg-accent disabled:opacity-50",
|
|
167
|
+
className
|
|
168
|
+
)}
|
|
169
|
+
onClick={handlePrev}
|
|
170
|
+
aria-label="Previous slide"
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
174
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
175
|
+
</svg>
|
|
176
|
+
</button>
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
CarouselPrevious.displayName = "CarouselPrevious"
|
|
180
|
+
|
|
181
|
+
const CarouselNext = React.forwardRef<
|
|
182
|
+
HTMLButtonElement,
|
|
183
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
184
|
+
>(({ className, ...props }, ref) => {
|
|
185
|
+
const context = React.useContext(CarouselContext)
|
|
186
|
+
if (!context) throw new Error("CarouselNext must be used within Carousel")
|
|
187
|
+
|
|
188
|
+
const handleNext = () => {
|
|
189
|
+
context.setIsTransitioning(true)
|
|
190
|
+
context.setCurrentIndex((prev) => prev + 1)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<button
|
|
195
|
+
ref={ref}
|
|
196
|
+
type="button"
|
|
197
|
+
className={cn(
|
|
198
|
+
"absolute right-4 top-1/2 -translate-y-1/2 h-8 w-8 rounded-full border bg-background shadow-md flex items-center justify-center cursor-pointer",
|
|
199
|
+
"hover:bg-accent disabled:opacity-50",
|
|
200
|
+
className
|
|
201
|
+
)}
|
|
202
|
+
onClick={handleNext}
|
|
203
|
+
aria-label="Next slide"
|
|
204
|
+
{...props}
|
|
205
|
+
>
|
|
206
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
207
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
208
|
+
</svg>
|
|
209
|
+
</button>
|
|
210
|
+
)
|
|
211
|
+
})
|
|
212
|
+
CarouselNext.displayName = "CarouselNext"
|
|
213
|
+
|
|
214
|
+
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }
|