@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.
- package/dist/index.mjs +215 -214
- package/package.json +4 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +557 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +14 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-guard.tsx +177 -0
- package/src/utils/license-validator.tsx +183 -0
- 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 };
|