@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
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
import { FaComments, FaTimes, FaPaperPlane } from "react-icons/fa"
|
|
5
6
|
import { cn } from "@/lib/utils"
|
|
6
7
|
|
|
7
8
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
9
|
+
import { Button } from "@/components/ui/button"
|
|
10
|
+
import { Input } from "@/components/ui/input"
|
|
11
|
+
import { LuMessageSquare, LuX } from "react-icons/lu"
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
// --- Primitives ---
|
|
14
|
+
|
|
15
|
+
const ChatbotContainer = React.forwardRef<
|
|
10
16
|
HTMLDivElement,
|
|
11
17
|
React.HTMLAttributes<HTMLDivElement>
|
|
12
18
|
>(({ className, ...props }, ref) => (
|
|
13
19
|
<div
|
|
14
20
|
ref={ref}
|
|
15
21
|
className={cn(
|
|
16
|
-
"flex flex-col w-full
|
|
22
|
+
"flex flex-col w-full border rounded-xl bg-background text-foreground shadow-2xl overflow-hidden",
|
|
17
23
|
className
|
|
18
24
|
)}
|
|
19
25
|
{...props}
|
|
20
26
|
/>
|
|
21
27
|
))
|
|
22
|
-
|
|
28
|
+
ChatbotContainer.displayName = "ChatbotContainer"
|
|
23
29
|
|
|
24
30
|
const ChatbotHeader = React.forwardRef<
|
|
25
31
|
HTMLDivElement,
|
|
@@ -28,7 +34,7 @@ const ChatbotHeader = React.forwardRef<
|
|
|
28
34
|
<div
|
|
29
35
|
ref={ref}
|
|
30
36
|
className={cn(
|
|
31
|
-
"flex items-center px-4 py-3 border-b bg-muted/40",
|
|
37
|
+
"flex items-center justify-between px-4 py-3 border-b bg-muted/40",
|
|
32
38
|
className
|
|
33
39
|
)}
|
|
34
40
|
{...props}
|
|
@@ -42,7 +48,7 @@ const ChatbotContent = React.forwardRef<
|
|
|
42
48
|
>(({ className, ...props }, ref) => (
|
|
43
49
|
<ScrollArea
|
|
44
50
|
ref={ref}
|
|
45
|
-
className={cn("flex-1 p-
|
|
51
|
+
className={cn("flex-1 p-4", className)}
|
|
46
52
|
scrollbarSize="thin"
|
|
47
53
|
{...props}
|
|
48
54
|
/>
|
|
@@ -50,7 +56,7 @@ const ChatbotContent = React.forwardRef<
|
|
|
50
56
|
ChatbotContent.displayName = "ChatbotContent"
|
|
51
57
|
|
|
52
58
|
const chatbotMessageVariants = cva(
|
|
53
|
-
"max-w-[
|
|
59
|
+
"max-w-[85%] rounded-2xl px-4 py-2 text-sm break-words shadow-sm",
|
|
54
60
|
{
|
|
55
61
|
variants: {
|
|
56
62
|
variant: {
|
|
@@ -71,9 +77,14 @@ const ChatbotMessage = React.forwardRef<
|
|
|
71
77
|
>(({ className, variant, ...props }, ref) => (
|
|
72
78
|
<div
|
|
73
79
|
ref={ref}
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
80
|
+
// Wrapper div for alignment if needed, but styling handles float-like behavior
|
|
81
|
+
className={cn("mb-4 flex", variant === "user" ? "justify-end" : "justify-start")}
|
|
82
|
+
>
|
|
83
|
+
<div
|
|
84
|
+
className={cn(chatbotMessageVariants({ variant }), className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
77
88
|
))
|
|
78
89
|
ChatbotMessage.displayName = "ChatbotMessage"
|
|
79
90
|
|
|
@@ -83,17 +94,79 @@ const ChatbotFooter = React.forwardRef<
|
|
|
83
94
|
>(({ className, ...props }, ref) => (
|
|
84
95
|
<div
|
|
85
96
|
ref={ref}
|
|
86
|
-
className={cn("flex items-center p-3 border-t bg-background
|
|
97
|
+
className={cn("flex items-center gap-2 p-3 border-t bg-background", className)}
|
|
87
98
|
{...props}
|
|
88
99
|
/>
|
|
89
100
|
))
|
|
90
101
|
ChatbotFooter.displayName = "ChatbotFooter"
|
|
91
102
|
|
|
103
|
+
// --- Main Widget Component ---
|
|
104
|
+
|
|
105
|
+
export function Chatbot() {
|
|
106
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
{/* Chat Window - Fixed Position (left of dock) */}
|
|
111
|
+
{isOpen && (
|
|
112
|
+
<div className="fixed bottom-4 right-16 md:bottom-6 md:right-32 z-50 w-[calc(100vw-80px)] sm:w-[350px] h-[500px] max-h-[70vh] animate-in slide-in-from-right-10 fade-in duration-300">
|
|
113
|
+
<ChatbotContainer className="h-full">
|
|
114
|
+
<ChatbotHeader>
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
<div className="h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
117
|
+
<span className="font-semibold text-sm">AI Assistant</span>
|
|
118
|
+
</div>
|
|
119
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setIsOpen(false)}>
|
|
120
|
+
<FaTimes className="h-4 w-4" />
|
|
121
|
+
</Button>
|
|
122
|
+
</ChatbotHeader>
|
|
123
|
+
|
|
124
|
+
<ChatbotContent>
|
|
125
|
+
<div className="flex flex-col">
|
|
126
|
+
<ChatbotMessage variant="bot">
|
|
127
|
+
Hello! 👋 I'm your AI assistant. How can I help you build your next project?
|
|
128
|
+
</ChatbotMessage>
|
|
129
|
+
<ChatbotMessage variant="user">
|
|
130
|
+
I need help with the new dashboard layout.
|
|
131
|
+
</ChatbotMessage>
|
|
132
|
+
<ChatbotMessage variant="bot">
|
|
133
|
+
Sure thing! I can help you with components, layout structures, or styling. What specifically are you looking for?
|
|
134
|
+
</ChatbotMessage>
|
|
135
|
+
</div>
|
|
136
|
+
</ChatbotContent>
|
|
137
|
+
|
|
138
|
+
<ChatbotFooter>
|
|
139
|
+
<Input placeholder="Type a message..." className="flex-1" />
|
|
140
|
+
<Button size="icon" type="submit">
|
|
141
|
+
<FaPaperPlane className="h-4 w-4" />
|
|
142
|
+
</Button>
|
|
143
|
+
</ChatbotFooter>
|
|
144
|
+
</ChatbotContainer>
|
|
145
|
+
</div >
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
{/* Trigger Button */}
|
|
150
|
+
<Button
|
|
151
|
+
variant="default"
|
|
152
|
+
size="icon"
|
|
153
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
154
|
+
className={cn(
|
|
155
|
+
"rounded-full h-10 w-10 md:h-16 md:w-16 shadow-lg transition-all duration-300 hover:scale-110",
|
|
156
|
+
isOpen ? "bg-muted text-foreground hover:bg-muted/80" : "bg-primary text-primary-foreground"
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
{isOpen ? <LuX className="!h-5 !w-5 md:!h-8 md:!w-8" /> : <LuMessageSquare className="!h-5 !w-5 md:!h-8 md:!w-8" />}
|
|
160
|
+
</Button>
|
|
161
|
+
</>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Export parts if still needed by other consumers (optional, but good practice)
|
|
92
166
|
export {
|
|
93
|
-
|
|
167
|
+
ChatbotContainer,
|
|
94
168
|
ChatbotHeader,
|
|
95
169
|
ChatbotContent,
|
|
96
170
|
ChatbotMessage,
|
|
97
171
|
ChatbotFooter,
|
|
98
172
|
}
|
|
99
|
-
|
|
@@ -1,94 +1,93 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
|
|
6
|
-
interface CheckboxProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
|
|
7
|
-
/**
|
|
8
|
-
* Whether the checkbox is checked
|
|
9
|
-
*/
|
|
10
|
-
checked?: boolean
|
|
11
|
-
/**
|
|
12
|
-
* Callback when the checked state changes
|
|
13
|
-
*/
|
|
14
|
-
onCheckedChange?: (checked: boolean) => void
|
|
15
|
-
/**
|
|
16
|
-
* Whether the checkbox is disabled
|
|
17
|
-
*/
|
|
18
|
-
disabled?: boolean
|
|
19
|
-
/**
|
|
20
|
-
* The default checked state (for uncontrolled mode)
|
|
21
|
-
*/
|
|
22
|
-
defaultChecked?: boolean
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Checkbox component with keyboard accessibility
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* const [checked, setChecked] = useState(false)
|
|
30
|
-
* <Checkbox checked={checked} onCheckedChange={setChecked} />
|
|
31
|
-
*/
|
|
32
|
-
const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
33
|
-
({ className, checked: controlledChecked, defaultChecked = false, onCheckedChange, disabled, ...props }, ref) => {
|
|
34
|
-
const [isChecked, setIsChecked] = React.useState(defaultChecked)
|
|
35
|
-
|
|
36
|
-
const checked = controlledChecked !== undefined ? controlledChecked : isChecked
|
|
37
|
-
|
|
38
|
-
const handleClick = () => {
|
|
39
|
-
if (!disabled) {
|
|
40
|
-
const newChecked = !checked
|
|
41
|
-
if (controlledChecked === undefined) {
|
|
42
|
-
setIsChecked(newChecked)
|
|
43
|
-
}
|
|
44
|
-
onCheckedChange?.(newChecked)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
49
|
-
if (e.key === " " || e.key === "Enter") {
|
|
50
|
-
e.preventDefault()
|
|
51
|
-
handleClick()
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<button
|
|
57
|
-
type="button"
|
|
58
|
-
role="checkbox"
|
|
59
|
-
aria-checked={checked}
|
|
60
|
-
aria-disabled={disabled}
|
|
61
|
-
disabled={disabled}
|
|
62
|
-
ref={ref}
|
|
63
|
-
className={cn(
|
|
64
|
-
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
65
|
-
checked && "bg-primary text-primary-foreground",
|
|
66
|
-
className
|
|
67
|
-
)}
|
|
68
|
-
onClick={handleClick}
|
|
69
|
-
onKeyDown={handleKeyDown}
|
|
70
|
-
{...props}
|
|
71
|
-
>
|
|
72
|
-
{checked && (
|
|
73
|
-
<svg
|
|
74
|
-
className="h-full w-full"
|
|
75
|
-
fill="none"
|
|
76
|
-
viewBox="0 0 24 24"
|
|
77
|
-
stroke="currentColor"
|
|
78
|
-
strokeWidth={3}
|
|
79
|
-
>
|
|
80
|
-
<path
|
|
81
|
-
strokeLinecap="round"
|
|
82
|
-
strokeLinejoin="round"
|
|
83
|
-
d="M5 13l4 4L19 7"
|
|
84
|
-
/>
|
|
85
|
-
</svg>
|
|
86
|
-
)}
|
|
87
|
-
</button>
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
)
|
|
91
|
-
Checkbox.displayName = "Checkbox"
|
|
92
|
-
|
|
93
|
-
export { Checkbox }
|
|
94
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
|
|
6
|
+
interface CheckboxProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
|
|
7
|
+
/**
|
|
8
|
+
* Whether the checkbox is checked
|
|
9
|
+
*/
|
|
10
|
+
checked?: boolean
|
|
11
|
+
/**
|
|
12
|
+
* Callback when the checked state changes
|
|
13
|
+
*/
|
|
14
|
+
onCheckedChange?: (checked: boolean) => void
|
|
15
|
+
/**
|
|
16
|
+
* Whether the checkbox is disabled
|
|
17
|
+
*/
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
/**
|
|
20
|
+
* The default checked state (for uncontrolled mode)
|
|
21
|
+
*/
|
|
22
|
+
defaultChecked?: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checkbox component with keyboard accessibility
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const [checked, setChecked] = useState(false)
|
|
30
|
+
* <Checkbox checked={checked} onCheckedChange={setChecked} />
|
|
31
|
+
*/
|
|
32
|
+
const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
33
|
+
({ className, checked: controlledChecked, defaultChecked = false, onCheckedChange, disabled, ...props }, ref) => {
|
|
34
|
+
const [isChecked, setIsChecked] = React.useState(defaultChecked)
|
|
35
|
+
|
|
36
|
+
const checked = controlledChecked !== undefined ? controlledChecked : isChecked
|
|
37
|
+
|
|
38
|
+
const handleClick = () => {
|
|
39
|
+
if (!disabled) {
|
|
40
|
+
const newChecked = !checked
|
|
41
|
+
if (controlledChecked === undefined) {
|
|
42
|
+
setIsChecked(newChecked)
|
|
43
|
+
}
|
|
44
|
+
onCheckedChange?.(newChecked)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
49
|
+
if (e.key === " " || e.key === "Enter") {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
handleClick()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
role="checkbox"
|
|
59
|
+
aria-checked={checked}
|
|
60
|
+
aria-disabled={disabled}
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cn(
|
|
64
|
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
65
|
+
checked && "bg-primary text-primary-foreground",
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
onClick={handleClick}
|
|
69
|
+
onKeyDown={handleKeyDown}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
{checked && (
|
|
73
|
+
<svg
|
|
74
|
+
className="h-full w-full"
|
|
75
|
+
fill="none"
|
|
76
|
+
viewBox="0 0 24 24"
|
|
77
|
+
stroke="currentColor"
|
|
78
|
+
strokeWidth={3}
|
|
79
|
+
>
|
|
80
|
+
<path
|
|
81
|
+
strokeLinecap="round"
|
|
82
|
+
strokeLinejoin="round"
|
|
83
|
+
d="M5 13l4 4L19 7"
|
|
84
|
+
/>
|
|
85
|
+
</svg>
|
|
86
|
+
)}
|
|
87
|
+
</button>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
Checkbox.displayName = "Checkbox"
|
|
92
|
+
|
|
93
|
+
export { Checkbox }
|
|
@@ -1,108 +1,107 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
5
|
-
import { Slot } from "@/components/ui/slot"
|
|
6
|
-
|
|
7
|
-
interface CollapsibleContextValue {
|
|
8
|
-
open: boolean
|
|
9
|
-
onOpenChange: (open: boolean) => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const CollapsibleContext = React.createContext<CollapsibleContextValue | null>(null)
|
|
13
|
-
|
|
14
|
-
interface CollapsibleProps {
|
|
15
|
-
children: React.ReactNode
|
|
16
|
-
open?: boolean
|
|
17
|
-
onOpenChange?: (open: boolean) => void
|
|
18
|
-
defaultOpen?: boolean
|
|
19
|
-
className?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Collapsible component for expandable content
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* <Collapsible>
|
|
27
|
-
* <CollapsibleTrigger>Toggle</CollapsibleTrigger>
|
|
28
|
-
* <CollapsibleContent>
|
|
29
|
-
* Hidden content here
|
|
30
|
-
* </CollapsibleContent>
|
|
31
|
-
* </Collapsible>
|
|
32
|
-
*/
|
|
33
|
-
const Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(
|
|
34
|
-
({ children, open: controlledOpen, onOpenChange, defaultOpen = false, className }, ref) => {
|
|
35
|
-
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
36
|
-
|
|
37
|
-
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
38
|
-
const setOpen = onOpenChange || setUncontrolledOpen
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<CollapsibleContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
42
|
-
<div ref={ref} className={className} data-state={open ? "open" : "closed"}>
|
|
43
|
-
{children}
|
|
44
|
-
</div>
|
|
45
|
-
</CollapsibleContext.Provider>
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
)
|
|
49
|
-
Collapsible.displayName = "Collapsible"
|
|
50
|
-
|
|
51
|
-
interface CollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
52
|
-
asChild?: boolean
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
|
|
56
|
-
({ className, children, asChild, onClick, ...props }, ref) => {
|
|
57
|
-
const context = React.useContext(CollapsibleContext)
|
|
58
|
-
if (!context) throw new Error("CollapsibleTrigger must be used within Collapsible")
|
|
59
|
-
|
|
60
|
-
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
61
|
-
onClick?.(e)
|
|
62
|
-
context.onOpenChange(!context.open)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const Comp = asChild ? Slot : "button"
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<Comp
|
|
69
|
-
ref={ref}
|
|
70
|
-
type="button"
|
|
71
|
-
aria-expanded={context.open}
|
|
72
|
-
className={className}
|
|
73
|
-
onClick={handleClick}
|
|
74
|
-
{...props}
|
|
75
|
-
>
|
|
76
|
-
{children}
|
|
77
|
-
</Comp>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
CollapsibleTrigger.displayName = "CollapsibleTrigger"
|
|
82
|
-
|
|
83
|
-
const CollapsibleContent = React.forwardRef<
|
|
84
|
-
HTMLDivElement,
|
|
85
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
86
|
-
>(({ className, children, ...props }, ref) => {
|
|
87
|
-
const context = React.useContext(CollapsibleContext)
|
|
88
|
-
if (!context) throw new Error("CollapsibleContent must be used within Collapsible")
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<div
|
|
92
|
-
ref={ref}
|
|
93
|
-
className={cn(
|
|
94
|
-
"overflow-hidden transition-all",
|
|
95
|
-
context.open ? "animate-collapsible-down" : "animate-collapsible-up hidden",
|
|
96
|
-
className
|
|
97
|
-
)}
|
|
98
|
-
data-state={context.open ? "open" : "closed"}
|
|
99
|
-
{...props}
|
|
100
|
-
>
|
|
101
|
-
{children}
|
|
102
|
-
</div>
|
|
103
|
-
)
|
|
104
|
-
})
|
|
105
|
-
CollapsibleContent.displayName = "CollapsibleContent"
|
|
106
|
-
|
|
107
|
-
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
|
108
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { Slot } from "@/components/ui/slot"
|
|
6
|
+
|
|
7
|
+
interface CollapsibleContextValue {
|
|
8
|
+
open: boolean
|
|
9
|
+
onOpenChange: (open: boolean) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CollapsibleContext = React.createContext<CollapsibleContextValue | null>(null)
|
|
13
|
+
|
|
14
|
+
interface CollapsibleProps {
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
open?: boolean
|
|
17
|
+
onOpenChange?: (open: boolean) => void
|
|
18
|
+
defaultOpen?: boolean
|
|
19
|
+
className?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Collapsible component for expandable content
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <Collapsible>
|
|
27
|
+
* <CollapsibleTrigger>Toggle</CollapsibleTrigger>
|
|
28
|
+
* <CollapsibleContent>
|
|
29
|
+
* Hidden content here
|
|
30
|
+
* </CollapsibleContent>
|
|
31
|
+
* </Collapsible>
|
|
32
|
+
*/
|
|
33
|
+
const Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(
|
|
34
|
+
({ children, open: controlledOpen, onOpenChange, defaultOpen = false, className }, ref) => {
|
|
35
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
36
|
+
|
|
37
|
+
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
38
|
+
const setOpen = onOpenChange || setUncontrolledOpen
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<CollapsibleContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
42
|
+
<div ref={ref} className={className} data-state={open ? "open" : "closed"}>
|
|
43
|
+
{children}
|
|
44
|
+
</div>
|
|
45
|
+
</CollapsibleContext.Provider>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
Collapsible.displayName = "Collapsible"
|
|
50
|
+
|
|
51
|
+
interface CollapsibleTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
52
|
+
asChild?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
|
|
56
|
+
({ className, children, asChild, onClick, ...props }, ref) => {
|
|
57
|
+
const context = React.useContext(CollapsibleContext)
|
|
58
|
+
if (!context) throw new Error("CollapsibleTrigger must be used within Collapsible")
|
|
59
|
+
|
|
60
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
61
|
+
onClick?.(e)
|
|
62
|
+
context.onOpenChange(!context.open)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const Comp = asChild ? Slot : "button"
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Comp
|
|
69
|
+
ref={ref}
|
|
70
|
+
type="button"
|
|
71
|
+
aria-expanded={context.open}
|
|
72
|
+
className={className}
|
|
73
|
+
onClick={handleClick}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
</Comp>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
CollapsibleTrigger.displayName = "CollapsibleTrigger"
|
|
82
|
+
|
|
83
|
+
const CollapsibleContent = React.forwardRef<
|
|
84
|
+
HTMLDivElement,
|
|
85
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
86
|
+
>(({ className, children, ...props }, ref) => {
|
|
87
|
+
const context = React.useContext(CollapsibleContext)
|
|
88
|
+
if (!context) throw new Error("CollapsibleContent must be used within Collapsible")
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
ref={ref}
|
|
93
|
+
className={cn(
|
|
94
|
+
"overflow-hidden transition-all",
|
|
95
|
+
context.open ? "animate-collapsible-down" : "animate-collapsible-up hidden",
|
|
96
|
+
className
|
|
97
|
+
)}
|
|
98
|
+
data-state={context.open ? "open" : "closed"}
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
CollapsibleContent.displayName = "CollapsibleContent"
|
|
106
|
+
|
|
107
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|