@moontra/moonui-pro 2.0.22 → 2.1.0

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.
Files changed (99) hide show
  1. package/dist/index.mjs +215 -214
  2. package/package.json +4 -2
  3. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  4. package/src/__tests__/use-local-storage.test.tsx +174 -0
  5. package/src/__tests__/use-pro-access.test.tsx +183 -0
  6. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  7. package/src/components/advanced-chart/index.tsx +412 -0
  8. package/src/components/advanced-forms/index.tsx +431 -0
  9. package/src/components/animated-button/index.tsx +202 -0
  10. package/src/components/calendar/event-dialog.tsx +372 -0
  11. package/src/components/calendar/index.tsx +557 -0
  12. package/src/components/color-picker/index.tsx +434 -0
  13. package/src/components/dashboard/index.tsx +334 -0
  14. package/src/components/data-table/data-table.test.tsx +187 -0
  15. package/src/components/data-table/index.tsx +368 -0
  16. package/src/components/draggable-list/index.tsx +100 -0
  17. package/src/components/enhanced/button.tsx +360 -0
  18. package/src/components/enhanced/card.tsx +272 -0
  19. package/src/components/enhanced/dialog.tsx +248 -0
  20. package/src/components/enhanced/index.ts +3 -0
  21. package/src/components/error-boundary/index.tsx +111 -0
  22. package/src/components/file-upload/file-upload.test.tsx +242 -0
  23. package/src/components/file-upload/index.tsx +362 -0
  24. package/src/components/floating-action-button/index.tsx +209 -0
  25. package/src/components/github-stars/index.tsx +414 -0
  26. package/src/components/health-check/index.tsx +441 -0
  27. package/src/components/hover-card-3d/index.tsx +170 -0
  28. package/src/components/index.ts +76 -0
  29. package/src/components/kanban/index.tsx +436 -0
  30. package/src/components/lazy-component/index.tsx +342 -0
  31. package/src/components/magnetic-button/index.tsx +170 -0
  32. package/src/components/memory-efficient-data/index.tsx +352 -0
  33. package/src/components/optimized-image/index.tsx +427 -0
  34. package/src/components/performance-debugger/index.tsx +591 -0
  35. package/src/components/performance-monitor/index.tsx +775 -0
  36. package/src/components/pinch-zoom/index.tsx +172 -0
  37. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  38. package/src/components/rich-text-editor/index.tsx +1537 -0
  39. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  40. package/src/components/rich-text-editor/slash-commands.css +35 -0
  41. package/src/components/rich-text-editor/table-styles.css +65 -0
  42. package/src/components/spotlight-card/index.tsx +194 -0
  43. package/src/components/swipeable-card/index.tsx +100 -0
  44. package/src/components/timeline/index.tsx +333 -0
  45. package/src/components/ui/animated-button.tsx +185 -0
  46. package/src/components/ui/avatar.tsx +135 -0
  47. package/src/components/ui/badge.tsx +225 -0
  48. package/src/components/ui/button.tsx +221 -0
  49. package/src/components/ui/card.tsx +141 -0
  50. package/src/components/ui/checkbox.tsx +256 -0
  51. package/src/components/ui/color-picker.tsx +95 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/hover-card-3d.tsx +103 -0
  55. package/src/components/ui/index.ts +33 -0
  56. package/src/components/ui/input.tsx +219 -0
  57. package/src/components/ui/label.tsx +26 -0
  58. package/src/components/ui/magnetic-button.tsx +129 -0
  59. package/src/components/ui/popover.tsx +183 -0
  60. package/src/components/ui/select.tsx +273 -0
  61. package/src/components/ui/separator.tsx +140 -0
  62. package/src/components/ui/slider.tsx +351 -0
  63. package/src/components/ui/spotlight-card.tsx +119 -0
  64. package/src/components/ui/switch.tsx +83 -0
  65. package/src/components/ui/tabs.tsx +195 -0
  66. package/src/components/ui/textarea.tsx +25 -0
  67. package/src/components/ui/toast.tsx +313 -0
  68. package/src/components/ui/tooltip.tsx +152 -0
  69. package/src/components/virtual-list/index.tsx +369 -0
  70. package/src/hooks/use-chart.ts +205 -0
  71. package/src/hooks/use-data-table.ts +182 -0
  72. package/src/hooks/use-docs-pro-access.ts +13 -0
  73. package/src/hooks/use-license-check.ts +65 -0
  74. package/src/hooks/use-subscription.ts +19 -0
  75. package/src/index.ts +14 -0
  76. package/src/lib/micro-interactions.ts +255 -0
  77. package/src/lib/utils.ts +6 -0
  78. package/src/patterns/login-form/index.tsx +276 -0
  79. package/src/patterns/login-form/types.ts +67 -0
  80. package/src/setupTests.ts +41 -0
  81. package/src/styles/design-system.css +365 -0
  82. package/src/styles/index.css +4 -0
  83. package/src/styles/tailwind.css +6 -0
  84. package/src/styles/tokens.css +453 -0
  85. package/src/types/moonui.d.ts +22 -0
  86. package/src/use-intersection-observer.tsx +154 -0
  87. package/src/use-local-storage.tsx +71 -0
  88. package/src/use-paddle.ts +138 -0
  89. package/src/use-performance-optimizer.ts +379 -0
  90. package/src/use-pro-access.ts +141 -0
  91. package/src/use-scroll-animation.ts +221 -0
  92. package/src/use-subscription.ts +37 -0
  93. package/src/use-toast.ts +32 -0
  94. package/src/utils/chart-helpers.ts +257 -0
  95. package/src/utils/cn.ts +69 -0
  96. package/src/utils/data-processing.ts +151 -0
  97. package/src/utils/license-guard.tsx +177 -0
  98. package/src/utils/license-validator.tsx +183 -0
  99. package/src/utils/package-guard.ts +60 -0
@@ -0,0 +1,333 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
5
+ import { Badge } from '../ui/badge'
6
+ import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'
7
+ import { Button } from '../ui/button'
8
+ import {
9
+ Clock,
10
+ CheckCircle2,
11
+ AlertCircle,
12
+ XCircle,
13
+ Circle,
14
+ Calendar,
15
+ User,
16
+ MessageCircle,
17
+ Paperclip,
18
+ ExternalLink,
19
+ Lock,
20
+ Sparkles
21
+ } from 'lucide-react'
22
+ import { cn } from '@moontra/moonui'
23
+
24
+ export interface TimelineEvent {
25
+ id: string
26
+ title: string
27
+ description?: string
28
+ date: Date
29
+ type: 'success' | 'warning' | 'error' | 'info' | 'pending'
30
+ user?: {
31
+ name: string
32
+ avatar?: string
33
+ email?: string
34
+ }
35
+ metadata?: {
36
+ location?: string
37
+ duration?: string
38
+ tags?: string[]
39
+ attachments?: number
40
+ comments?: number
41
+ externalLink?: string
42
+ }
43
+ icon?: React.ReactNode
44
+ color?: string
45
+ }
46
+
47
+ export interface TimelineProps {
48
+ events: TimelineEvent[]
49
+ onEventClick?: (event: TimelineEvent) => void
50
+ className?: string
51
+ showUserInfo?: boolean
52
+ showMetadata?: boolean
53
+ showRelativeTime?: boolean
54
+ groupByDate?: boolean
55
+ orientation?: 'vertical' | 'horizontal'
56
+ compact?: boolean
57
+ interactive?: boolean
58
+ }
59
+
60
+ const EVENT_COLORS = {
61
+ success: 'bg-green-500 border-green-500',
62
+ warning: 'bg-yellow-500 border-yellow-500',
63
+ error: 'bg-red-500 border-red-500',
64
+ info: 'bg-blue-500 border-blue-500',
65
+ pending: 'bg-gray-400 border-gray-400'
66
+ }
67
+
68
+ const EVENT_ICONS = {
69
+ success: <CheckCircle2 className="h-4 w-4 text-white" />,
70
+ warning: <AlertCircle className="h-4 w-4 text-white" />,
71
+ error: <XCircle className="h-4 w-4 text-white" />,
72
+ info: <Circle className="h-4 w-4 text-white" />,
73
+ pending: <Clock className="h-4 w-4 text-white" />
74
+ }
75
+
76
+ const EVENT_TEXT_COLORS = {
77
+ success: 'text-green-700',
78
+ warning: 'text-yellow-700',
79
+ error: 'text-red-700',
80
+ info: 'text-blue-700',
81
+ pending: 'text-gray-700'
82
+ }
83
+
84
+ export function Timeline({
85
+ events,
86
+ onEventClick,
87
+ className,
88
+ showUserInfo = true,
89
+ showMetadata = true,
90
+ showRelativeTime = true,
91
+ groupByDate = false,
92
+ orientation = 'vertical',
93
+ compact = false,
94
+ interactive = true
95
+ }: TimelineProps) {
96
+ const sortedEvents = [...events].sort((a, b) => b.date.getTime() - a.date.getTime())
97
+
98
+ const formatDate = (date: Date) => {
99
+ return date.toLocaleDateString('en-US', {
100
+ year: 'numeric',
101
+ month: 'long',
102
+ day: 'numeric'
103
+ })
104
+ }
105
+
106
+ const formatTime = (date: Date) => {
107
+ return date.toLocaleTimeString('en-US', {
108
+ hour: '2-digit',
109
+ minute: '2-digit',
110
+ hour12: true
111
+ })
112
+ }
113
+
114
+ const getRelativeTime = (date: Date) => {
115
+ const now = new Date()
116
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
117
+
118
+ if (diffInSeconds < 60) {
119
+ return 'Just now'
120
+ } else if (diffInSeconds < 3600) {
121
+ const minutes = Math.floor(diffInSeconds / 60)
122
+ return `${minutes} minute${minutes > 1 ? 's' : ''} ago`
123
+ } else if (diffInSeconds < 86400) {
124
+ const hours = Math.floor(diffInSeconds / 3600)
125
+ return `${hours} hour${hours > 1 ? 's' : ''} ago`
126
+ } else if (diffInSeconds < 2592000) {
127
+ const days = Math.floor(diffInSeconds / 86400)
128
+ return `${days} day${days > 1 ? 's' : ''} ago`
129
+ } else {
130
+ return formatDate(date)
131
+ }
132
+ }
133
+
134
+ const getInitials = (name: string) => {
135
+ return name.split(' ').map(n => n[0]).join('').toUpperCase()
136
+ }
137
+
138
+ const groupEventsByDate = (events: TimelineEvent[]) => {
139
+ const groups: { [key: string]: TimelineEvent[] } = {}
140
+
141
+ events.forEach(event => {
142
+ const dateKey = formatDate(event.date)
143
+ if (!groups[dateKey]) {
144
+ groups[dateKey] = []
145
+ }
146
+ groups[dateKey].push(event)
147
+ })
148
+
149
+ return groups
150
+ }
151
+
152
+ const handleEventClick = (event: TimelineEvent) => {
153
+ if (interactive && onEventClick) {
154
+ onEventClick(event)
155
+ }
156
+ }
157
+
158
+ const renderEvent = (event: TimelineEvent, index: number, isLast: boolean) => {
159
+ const eventColor = event.color || EVENT_COLORS[event.type]
160
+ const eventIcon = event.icon || EVENT_ICONS[event.type]
161
+ const textColor = EVENT_TEXT_COLORS[event.type]
162
+
163
+ return (
164
+ <div
165
+ key={event.id}
166
+ className={cn(
167
+ "relative flex gap-4",
168
+ compact ? "pb-4" : "pb-8",
169
+ interactive && "cursor-pointer hover:bg-muted/50 rounded-lg p-2 -m-2 transition-colors"
170
+ )}
171
+ onClick={() => handleEventClick(event)}
172
+ >
173
+ {/* Timeline Line */}
174
+ {orientation === 'vertical' && (
175
+ <div className="flex flex-col items-center">
176
+ <div className={cn(
177
+ "flex items-center justify-center w-8 h-8 rounded-full border-2 bg-background",
178
+ eventColor
179
+ )}>
180
+ {eventIcon}
181
+ </div>
182
+ {!isLast && (
183
+ <div className="w-0.5 h-full bg-border mt-2" />
184
+ )}
185
+ </div>
186
+ )}
187
+
188
+ {/* Event Content */}
189
+ <div className="flex-1 min-w-0">
190
+ <div className="flex items-start justify-between gap-2">
191
+ <div className="flex-1">
192
+ <h4 className={cn(
193
+ "font-medium text-sm",
194
+ compact ? "mb-1" : "mb-2"
195
+ )}>
196
+ {event.title}
197
+ </h4>
198
+ {event.description && (
199
+ <p className={cn(
200
+ "text-muted-foreground text-sm",
201
+ compact ? "mb-1" : "mb-2"
202
+ )}>
203
+ {event.description}
204
+ </p>
205
+ )}
206
+ </div>
207
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
208
+ {showRelativeTime ? (
209
+ <span>{getRelativeTime(event.date)}</span>
210
+ ) : (
211
+ <span>{formatTime(event.date)}</span>
212
+ )}
213
+ <Badge variant="outline" className={cn("text-xs", textColor)}>
214
+ {event.type}
215
+ </Badge>
216
+ </div>
217
+ </div>
218
+
219
+ {/* User Info */}
220
+ {showUserInfo && event.user && (
221
+ <div className="flex items-center gap-2 mb-2">
222
+ <Avatar className="h-6 w-6">
223
+ <AvatarImage src={event.user.avatar} />
224
+ <AvatarFallback className="text-xs">
225
+ {getInitials(event.user.name)}
226
+ </AvatarFallback>
227
+ </Avatar>
228
+ <span className="text-sm text-muted-foreground">
229
+ {event.user.name}
230
+ </span>
231
+ </div>
232
+ )}
233
+
234
+ {/* Metadata */}
235
+ {showMetadata && event.metadata && (
236
+ <div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
237
+ {event.metadata.duration && (
238
+ <div className="flex items-center gap-1">
239
+ <Clock className="h-3 w-3" />
240
+ <span>{event.metadata.duration}</span>
241
+ </div>
242
+ )}
243
+ {event.metadata.location && (
244
+ <div className="flex items-center gap-1">
245
+ <Calendar className="h-3 w-3" />
246
+ <span>{event.metadata.location}</span>
247
+ </div>
248
+ )}
249
+ {event.metadata.comments && event.metadata.comments > 0 && (
250
+ <div className="flex items-center gap-1">
251
+ <MessageCircle className="h-3 w-3" />
252
+ <span>{event.metadata.comments}</span>
253
+ </div>
254
+ )}
255
+ {event.metadata.attachments && event.metadata.attachments > 0 && (
256
+ <div className="flex items-center gap-1">
257
+ <Paperclip className="h-3 w-3" />
258
+ <span>{event.metadata.attachments}</span>
259
+ </div>
260
+ )}
261
+ {event.metadata.externalLink && (
262
+ <div className="flex items-center gap-1">
263
+ <ExternalLink className="h-3 w-3" />
264
+ <span>View details</span>
265
+ </div>
266
+ )}
267
+ </div>
268
+ )}
269
+
270
+ {/* Tags */}
271
+ {event.metadata?.tags && event.metadata.tags.length > 0 && (
272
+ <div className="flex flex-wrap gap-1 mt-2">
273
+ {event.metadata.tags.map((tag, tagIndex) => (
274
+ <Badge key={tagIndex} variant="secondary" className="text-xs">
275
+ {tag}
276
+ </Badge>
277
+ ))}
278
+ </div>
279
+ )}
280
+ </div>
281
+ </div>
282
+ )
283
+ }
284
+
285
+ const renderGroupedEvents = () => {
286
+ const groups = groupEventsByDate(sortedEvents)
287
+
288
+ return Object.entries(groups).map(([date, events], groupIndex) => (
289
+ <div key={date} className="mb-8">
290
+ <div className="sticky top-0 bg-background/80 backdrop-blur-sm border-b p-2 mb-4">
291
+ <h3 className="font-medium text-sm text-muted-foreground">{date}</h3>
292
+ </div>
293
+ {events.map((event, index) => renderEvent(event, index, index === events.length - 1))}
294
+ </div>
295
+ ))
296
+ }
297
+
298
+ return (
299
+ <Card className={cn("w-full", className)}>
300
+ <CardHeader>
301
+ <CardTitle className="flex items-center gap-2">
302
+ <Clock className="h-5 w-5" />
303
+ Timeline
304
+ </CardTitle>
305
+ <CardDescription>
306
+ {sortedEvents.length} event{sortedEvents.length !== 1 ? 's' : ''} tracked
307
+ </CardDescription>
308
+ </CardHeader>
309
+ <CardContent>
310
+ {sortedEvents.length > 0 ? (
311
+ <div className={cn(
312
+ "space-y-0",
313
+ orientation === 'horizontal' && "flex overflow-x-auto gap-6"
314
+ )}>
315
+ {groupByDate
316
+ ? renderGroupedEvents()
317
+ : sortedEvents.map((event, index) =>
318
+ renderEvent(event, index, index === sortedEvents.length - 1)
319
+ )
320
+ }
321
+ </div>
322
+ ) : (
323
+ <div className="text-center py-8">
324
+ <Clock className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
325
+ <p className="text-muted-foreground">No events to display</p>
326
+ </div>
327
+ )}
328
+ </CardContent>
329
+ </Card>
330
+ )
331
+ }
332
+
333
+ export default Timeline
@@ -0,0 +1,185 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+ import { Check, X, Loader2 } from "lucide-react";
6
+ import { cn } from "@/lib/utils";
7
+ import { cva, type VariantProps } from "class-variance-authority";
8
+
9
+ const animatedButtonVariants = cva(
10
+ "relative inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
15
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
16
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary: "bg-secondary text-secondary-foreground 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-10 px-4 py-2",
23
+ sm: "h-9 rounded-md px-3",
24
+ lg: "h-11 rounded-md px-8",
25
+ icon: "h-10 w-10",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ size: "default",
31
+ },
32
+ }
33
+ );
34
+
35
+ export interface AnimatedButtonProps
36
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
37
+ VariantProps<typeof animatedButtonVariants> {
38
+ state?: "idle" | "loading" | "success" | "error";
39
+ loadingText?: string;
40
+ successText?: string;
41
+ errorText?: string;
42
+ }
43
+
44
+ const AnimatedButton = React.forwardRef<HTMLButtonElement, AnimatedButtonProps>(
45
+ (
46
+ {
47
+ className,
48
+ variant,
49
+ size,
50
+ state = "idle",
51
+ loadingText = "Loading...",
52
+ successText = "Success!",
53
+ errorText = "Error!",
54
+ children,
55
+ disabled,
56
+ ...props
57
+ },
58
+ ref
59
+ ) => {
60
+ const [buttonWidth, setButtonWidth] = React.useState<number | "auto">("auto");
61
+ const buttonRef = React.useRef<HTMLButtonElement>(null);
62
+
63
+ React.useEffect(() => {
64
+ if (buttonRef.current && state === "idle") {
65
+ setButtonWidth(buttonRef.current.offsetWidth);
66
+ }
67
+ }, [state, children]);
68
+
69
+ const isDisabled = disabled || state !== "idle";
70
+
71
+ const contentVariants = {
72
+ idle: { opacity: 1, y: 0 },
73
+ loading: { opacity: 0, y: 10 },
74
+ success: { opacity: 0, y: 10 },
75
+ error: { opacity: 0, y: 10 },
76
+ };
77
+
78
+ const iconVariants = {
79
+ hidden: { opacity: 0, scale: 0.8, y: -10 },
80
+ visible: { opacity: 1, scale: 1, y: 0 },
81
+ };
82
+
83
+ return (
84
+ <motion.button
85
+ ref={(el) => {
86
+ buttonRef.current = el;
87
+ if (ref) {
88
+ if (typeof ref === "function") ref(el);
89
+ else ref.current = el;
90
+ }
91
+ }}
92
+ className={cn(animatedButtonVariants({ variant, size, className }))}
93
+ disabled={isDisabled}
94
+ animate={{
95
+ width: state !== "idle" ? buttonWidth : "auto",
96
+ }}
97
+ transition={{ duration: 0.2 }}
98
+ {...props}
99
+ >
100
+ <AnimatePresence mode="wait">
101
+ {state === "idle" && (
102
+ <motion.span
103
+ key="idle"
104
+ variants={contentVariants}
105
+ initial="loading"
106
+ animate="idle"
107
+ exit="loading"
108
+ transition={{ duration: 0.2 }}
109
+ className="inline-flex items-center"
110
+ >
111
+ {children}
112
+ </motion.span>
113
+ )}
114
+
115
+ {state === "loading" && (
116
+ <motion.span
117
+ key="loading"
118
+ className="inline-flex items-center gap-2"
119
+ initial={{ opacity: 0, y: -10 }}
120
+ animate={{ opacity: 1, y: 0 }}
121
+ exit={{ opacity: 0, y: 10 }}
122
+ transition={{ duration: 0.2 }}
123
+ >
124
+ <Loader2 className="h-4 w-4 animate-spin" />
125
+ {loadingText}
126
+ </motion.span>
127
+ )}
128
+
129
+ {state === "success" && (
130
+ <motion.span
131
+ key="success"
132
+ className="inline-flex items-center gap-2"
133
+ initial={{ opacity: 0, y: -10 }}
134
+ animate={{ opacity: 1, y: 0 }}
135
+ exit={{ opacity: 0, y: 10 }}
136
+ transition={{ duration: 0.2 }}
137
+ >
138
+ <motion.div
139
+ variants={iconVariants}
140
+ initial="hidden"
141
+ animate="visible"
142
+ transition={{ duration: 0.3, delay: 0.1 }}
143
+ >
144
+ <Check className="h-4 w-4" />
145
+ </motion.div>
146
+ {successText}
147
+ </motion.span>
148
+ )}
149
+
150
+ {state === "error" && (
151
+ <motion.span
152
+ key="error"
153
+ className="inline-flex items-center gap-2"
154
+ initial={{ opacity: 0, y: -10 }}
155
+ animate={{ opacity: 1, y: 0 }}
156
+ exit={{ opacity: 0, y: 10 }}
157
+ transition={{ duration: 0.2 }}
158
+ >
159
+ <motion.div
160
+ variants={iconVariants}
161
+ initial="hidden"
162
+ animate="visible"
163
+ transition={{ duration: 0.3, delay: 0.1 }}
164
+ animate={{
165
+ x: [0, -2, 2, -2, 2, 0],
166
+ }}
167
+ transition={{
168
+ duration: 0.4,
169
+ delay: 0.1,
170
+ }}
171
+ >
172
+ <X className="h-4 w-4" />
173
+ </motion.div>
174
+ {errorText}
175
+ </motion.span>
176
+ )}
177
+ </AnimatePresence>
178
+ </motion.button>
179
+ );
180
+ }
181
+ );
182
+
183
+ AnimatedButton.displayName = "AnimatedButton";
184
+
185
+ export { AnimatedButton, animatedButtonVariants };
@@ -0,0 +1,135 @@
1
+ import * as React from "react";
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ const avatarVariants = cva(
8
+ "relative flex shrink-0 overflow-hidden",
9
+ {
10
+ variants: {
11
+ size: {
12
+ default: "h-10 w-10",
13
+ xs: "h-6 w-6",
14
+ sm: "h-8 w-8",
15
+ md: "h-10 w-10",
16
+ lg: "h-12 w-12",
17
+ xl: "h-16 w-16",
18
+ "2xl": "h-20 w-20",
19
+ },
20
+ radius: {
21
+ default: "rounded-full",
22
+ sm: "rounded-md",
23
+ lg: "rounded-xl",
24
+ full: "rounded-full",
25
+ none: "rounded-none",
26
+ },
27
+ variant: {
28
+ default: "",
29
+ ring: "ring-2 ring-gray-300 dark:ring-gray-600",
30
+ ringOffset: "ring-2 ring-gray-300 dark:ring-gray-600 ring-offset-2 ring-offset-background dark:ring-offset-gray-950",
31
+ border: "border-2 border-gray-200 dark:border-gray-800"
32
+ }
33
+ },
34
+ defaultVariants: {
35
+ size: "default",
36
+ radius: "default",
37
+ variant: "default"
38
+ },
39
+ }
40
+ );
41
+
42
+ export interface AvatarProps
43
+ extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>,
44
+ VariantProps<typeof avatarVariants> {}
45
+
46
+ const Avatar = React.forwardRef<
47
+ React.ElementRef<typeof AvatarPrimitive.Root>,
48
+ AvatarProps
49
+ >(({ className, size, radius, variant, ...props }, ref) => (
50
+ <AvatarPrimitive.Root
51
+ ref={ref}
52
+ className={cn(avatarVariants({ size, radius, variant }), className)}
53
+ {...props}
54
+ />
55
+ ));
56
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
57
+
58
+ const AvatarImage = React.forwardRef<
59
+ React.ElementRef<typeof AvatarPrimitive.Image>,
60
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
61
+ >(({ className, ...props }, ref) => (
62
+ <AvatarPrimitive.Image
63
+ ref={ref}
64
+ className={cn("aspect-square h-full w-full", className)}
65
+ {...props}
66
+ />
67
+ ));
68
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
69
+
70
+ const AvatarFallback = React.forwardRef<
71
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
72
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
73
+ >(({ className, ...props }, ref) => (
74
+ <AvatarPrimitive.Fallback
75
+ ref={ref}
76
+ className={cn(
77
+ "flex h-full w-full items-center justify-center bg-muted",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ ));
83
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
84
+
85
+ // Avatar Group Component for displaying multiple avatars
86
+ interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
87
+ limit?: number;
88
+ avatars: React.ReactNode[];
89
+ overlapOffset?: number;
90
+ }
91
+
92
+ const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
93
+ ({ className, limit, avatars, overlapOffset = -8, ...props }, ref) => {
94
+ const visibleAvatars = limit ? avatars.slice(0, limit) : avatars;
95
+ const remainingCount = limit ? Math.max(0, avatars.length - limit) : 0;
96
+
97
+ return (
98
+ <div
99
+ ref={ref}
100
+ className={cn("flex items-center", className)}
101
+ {...props}
102
+ >
103
+ <div className="flex">
104
+ {visibleAvatars.map((avatar, index) => (
105
+ <div
106
+ key={index}
107
+ className="relative"
108
+ style={{
109
+ marginLeft: index === 0 ? 0 : `${overlapOffset}px`,
110
+ zIndex: visibleAvatars.length - index
111
+ }}
112
+ >
113
+ {avatar}
114
+ </div>
115
+ ))}
116
+ {remainingCount > 0 && (
117
+ <div
118
+ className="relative z-0"
119
+ style={{ marginLeft: `${overlapOffset}px` }}
120
+ >
121
+ <Avatar variant="border">
122
+ <AvatarFallback>
123
+ +{remainingCount}
124
+ </AvatarFallback>
125
+ </Avatar>
126
+ </div>
127
+ )}
128
+ </div>
129
+ </div>
130
+ );
131
+ }
132
+ );
133
+ AvatarGroup.displayName = "AvatarGroup";
134
+
135
+ export { Avatar, AvatarImage, AvatarFallback, AvatarGroup };