@moontra/moonui-pro 2.4.5 → 2.5.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.d.ts +127 -25
- package/dist/index.mjs +2023 -186
- package/package.json +4 -1
- package/src/components/advanced-chart/index.tsx +11 -1
- package/src/components/dashboard/dashboard-grid.tsx +327 -0
- package/src/components/dashboard/demo.tsx +308 -0
- package/src/components/dashboard/index.tsx +683 -275
- package/src/components/dashboard/time-range-picker.tsx +267 -0
- package/src/components/dashboard/types.ts +222 -0
- package/src/components/dashboard/widgets/activity-feed.tsx +344 -0
- package/src/components/dashboard/widgets/chart-widget.tsx +418 -0
- package/src/components/dashboard/widgets/metric-card.tsx +343 -0
- package/src/components/index.ts +2 -2
- package/src/components/ui/calendar.tsx +65 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/sheet.tsx +139 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card'
|
|
6
|
+
import { Button } from '../../ui/button'
|
|
7
|
+
import { cn } from '../../../lib/utils'
|
|
8
|
+
import {
|
|
9
|
+
LineChart,
|
|
10
|
+
Line,
|
|
11
|
+
BarChart,
|
|
12
|
+
Bar,
|
|
13
|
+
AreaChart,
|
|
14
|
+
Area,
|
|
15
|
+
PieChart,
|
|
16
|
+
Pie,
|
|
17
|
+
RadarChart,
|
|
18
|
+
PolarGrid,
|
|
19
|
+
PolarAngleAxis,
|
|
20
|
+
PolarRadiusAxis,
|
|
21
|
+
Radar,
|
|
22
|
+
XAxis,
|
|
23
|
+
YAxis,
|
|
24
|
+
CartesianGrid,
|
|
25
|
+
Tooltip,
|
|
26
|
+
Legend,
|
|
27
|
+
ResponsiveContainer,
|
|
28
|
+
Cell
|
|
29
|
+
} from 'recharts'
|
|
30
|
+
import {
|
|
31
|
+
MoreVertical,
|
|
32
|
+
Download,
|
|
33
|
+
Maximize2,
|
|
34
|
+
RefreshCw,
|
|
35
|
+
Palette,
|
|
36
|
+
Settings,
|
|
37
|
+
Filter
|
|
38
|
+
} from 'lucide-react'
|
|
39
|
+
import {
|
|
40
|
+
DropdownMenu,
|
|
41
|
+
DropdownMenuContent,
|
|
42
|
+
DropdownMenuItem,
|
|
43
|
+
DropdownMenuSeparator,
|
|
44
|
+
DropdownMenuTrigger,
|
|
45
|
+
DropdownMenuSub,
|
|
46
|
+
DropdownMenuSubContent,
|
|
47
|
+
DropdownMenuSubTrigger,
|
|
48
|
+
} from '../../ui/dropdown-menu'
|
|
49
|
+
import { ChartData } from '../types'
|
|
50
|
+
|
|
51
|
+
interface ChartWidgetProps {
|
|
52
|
+
id: string
|
|
53
|
+
title: string
|
|
54
|
+
description?: string
|
|
55
|
+
data: ChartData
|
|
56
|
+
className?: string
|
|
57
|
+
height?: number
|
|
58
|
+
onAction?: (action: string, data?: any) => void
|
|
59
|
+
loading?: boolean
|
|
60
|
+
refreshing?: boolean
|
|
61
|
+
glassmorphism?: boolean
|
|
62
|
+
interactive?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Renk paleti
|
|
66
|
+
const CHART_COLORS = {
|
|
67
|
+
default: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'],
|
|
68
|
+
pastel: ['#93c5fd', '#86efac', '#fcd34d', '#fca5a5', '#c4b5fd', '#fbcfe8'],
|
|
69
|
+
vibrant: ['#2563eb', '#059669', '#d97706', '#dc2626', '#7c3aed', '#db2777'],
|
|
70
|
+
monochrome: ['#1f2937', '#374151', '#4b5563', '#6b7280', '#9ca3af', '#d1d5db']
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function ChartWidget({
|
|
74
|
+
id,
|
|
75
|
+
title,
|
|
76
|
+
description,
|
|
77
|
+
data,
|
|
78
|
+
className,
|
|
79
|
+
height = 300,
|
|
80
|
+
onAction,
|
|
81
|
+
loading = false,
|
|
82
|
+
refreshing = false,
|
|
83
|
+
glassmorphism = false,
|
|
84
|
+
interactive = true
|
|
85
|
+
}: ChartWidgetProps) {
|
|
86
|
+
const [colorScheme, setColorScheme] = React.useState<keyof typeof CHART_COLORS>('default')
|
|
87
|
+
const [isFullscreen, setIsFullscreen] = React.useState(false)
|
|
88
|
+
|
|
89
|
+
const colors = CHART_COLORS[colorScheme]
|
|
90
|
+
|
|
91
|
+
// Custom tooltip
|
|
92
|
+
const CustomTooltip = ({ active, payload, label }: any) => {
|
|
93
|
+
if (!active || !payload) return null
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<motion.div
|
|
97
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
98
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
99
|
+
className={cn(
|
|
100
|
+
"p-3 rounded-lg shadow-lg",
|
|
101
|
+
glassmorphism
|
|
102
|
+
? "bg-background/80 backdrop-blur-md border border-white/10"
|
|
103
|
+
: "bg-background border"
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<p className="text-sm font-medium">{label}</p>
|
|
107
|
+
{payload.map((entry: any, index: number) => (
|
|
108
|
+
<p key={index} className="text-sm mt-1">
|
|
109
|
+
<span
|
|
110
|
+
className="inline-block w-3 h-3 rounded-full mr-2"
|
|
111
|
+
style={{ backgroundColor: entry.color }}
|
|
112
|
+
/>
|
|
113
|
+
{entry.name}: {entry.value}
|
|
114
|
+
</p>
|
|
115
|
+
))}
|
|
116
|
+
</motion.div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Chart renderer
|
|
121
|
+
const renderChart = () => {
|
|
122
|
+
if (!data.data || data.data.length === 0) {
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex items-center justify-center h-full text-muted-foreground">
|
|
125
|
+
No data available
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const chartProps = {
|
|
131
|
+
data: data.data,
|
|
132
|
+
margin: { top: 5, right: 30, left: 20, bottom: 5 }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
switch (data.type) {
|
|
136
|
+
case 'line':
|
|
137
|
+
return (
|
|
138
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
139
|
+
<LineChart {...chartProps}>
|
|
140
|
+
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
|
|
141
|
+
<XAxis dataKey="name" />
|
|
142
|
+
<YAxis />
|
|
143
|
+
<Tooltip content={<CustomTooltip />} />
|
|
144
|
+
{interactive && <Legend />}
|
|
145
|
+
{Object.keys(data.data[0])
|
|
146
|
+
.filter(key => key !== 'name')
|
|
147
|
+
.map((key, index) => (
|
|
148
|
+
<Line
|
|
149
|
+
key={key}
|
|
150
|
+
type="monotone"
|
|
151
|
+
dataKey={key}
|
|
152
|
+
stroke={colors[index % colors.length]}
|
|
153
|
+
strokeWidth={2}
|
|
154
|
+
dot={{ r: 4 }}
|
|
155
|
+
activeDot={{ r: 6 }}
|
|
156
|
+
/>
|
|
157
|
+
))}
|
|
158
|
+
</LineChart>
|
|
159
|
+
</ResponsiveContainer>
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
case 'bar':
|
|
163
|
+
return (
|
|
164
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
165
|
+
<BarChart {...chartProps}>
|
|
166
|
+
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
|
|
167
|
+
<XAxis dataKey="name" />
|
|
168
|
+
<YAxis />
|
|
169
|
+
<Tooltip content={<CustomTooltip />} />
|
|
170
|
+
{interactive && <Legend />}
|
|
171
|
+
{Object.keys(data.data[0])
|
|
172
|
+
.filter(key => key !== 'name')
|
|
173
|
+
.map((key, index) => (
|
|
174
|
+
<Bar
|
|
175
|
+
key={key}
|
|
176
|
+
dataKey={key}
|
|
177
|
+
fill={colors[index % colors.length]}
|
|
178
|
+
radius={[4, 4, 0, 0]}
|
|
179
|
+
/>
|
|
180
|
+
))}
|
|
181
|
+
</BarChart>
|
|
182
|
+
</ResponsiveContainer>
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
case 'area':
|
|
186
|
+
return (
|
|
187
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
188
|
+
<AreaChart {...chartProps}>
|
|
189
|
+
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
|
|
190
|
+
<XAxis dataKey="name" />
|
|
191
|
+
<YAxis />
|
|
192
|
+
<Tooltip content={<CustomTooltip />} />
|
|
193
|
+
{interactive && <Legend />}
|
|
194
|
+
{Object.keys(data.data[0])
|
|
195
|
+
.filter(key => key !== 'name')
|
|
196
|
+
.map((key, index) => (
|
|
197
|
+
<Area
|
|
198
|
+
key={key}
|
|
199
|
+
type="monotone"
|
|
200
|
+
dataKey={key}
|
|
201
|
+
stroke={colors[index % colors.length]}
|
|
202
|
+
fill={colors[index % colors.length]}
|
|
203
|
+
fillOpacity={0.2}
|
|
204
|
+
/>
|
|
205
|
+
))}
|
|
206
|
+
</AreaChart>
|
|
207
|
+
</ResponsiveContainer>
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
case 'pie':
|
|
211
|
+
case 'donut':
|
|
212
|
+
return (
|
|
213
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
214
|
+
<PieChart>
|
|
215
|
+
<Pie
|
|
216
|
+
data={data.data}
|
|
217
|
+
cx="50%"
|
|
218
|
+
cy="50%"
|
|
219
|
+
labelLine={false}
|
|
220
|
+
label={interactive}
|
|
221
|
+
outerRadius={data.type === 'donut' ? 80 : 100}
|
|
222
|
+
innerRadius={data.type === 'donut' ? 50 : 0}
|
|
223
|
+
fill="#8884d8"
|
|
224
|
+
dataKey="value"
|
|
225
|
+
>
|
|
226
|
+
{data.data.map((entry: any, index: number) => (
|
|
227
|
+
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
|
|
228
|
+
))}
|
|
229
|
+
</Pie>
|
|
230
|
+
<Tooltip content={<CustomTooltip />} />
|
|
231
|
+
{interactive && <Legend />}
|
|
232
|
+
</PieChart>
|
|
233
|
+
</ResponsiveContainer>
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
case 'radar':
|
|
237
|
+
return (
|
|
238
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
239
|
+
<RadarChart data={data.data}>
|
|
240
|
+
<PolarGrid />
|
|
241
|
+
<PolarAngleAxis dataKey="subject" />
|
|
242
|
+
<PolarRadiusAxis />
|
|
243
|
+
{Object.keys(data.data[0])
|
|
244
|
+
.filter(key => key !== 'subject')
|
|
245
|
+
.map((key, index) => (
|
|
246
|
+
<Radar
|
|
247
|
+
key={key}
|
|
248
|
+
name={key}
|
|
249
|
+
dataKey={key}
|
|
250
|
+
stroke={colors[index % colors.length]}
|
|
251
|
+
fill={colors[index % colors.length]}
|
|
252
|
+
fillOpacity={0.3}
|
|
253
|
+
/>
|
|
254
|
+
))}
|
|
255
|
+
<Tooltip content={<CustomTooltip />} />
|
|
256
|
+
{interactive && <Legend />}
|
|
257
|
+
</RadarChart>
|
|
258
|
+
</ResponsiveContainer>
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
default:
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Card variants
|
|
267
|
+
const cardVariants = {
|
|
268
|
+
initial: { opacity: 0, scale: 0.95 },
|
|
269
|
+
animate: {
|
|
270
|
+
opacity: 1,
|
|
271
|
+
scale: 1,
|
|
272
|
+
transition: { duration: 0.3, ease: "easeOut" }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<motion.div
|
|
278
|
+
variants={cardVariants}
|
|
279
|
+
initial="initial"
|
|
280
|
+
animate="animate"
|
|
281
|
+
className={cn(isFullscreen && "fixed inset-4 z-50")}
|
|
282
|
+
>
|
|
283
|
+
<Card className={cn(
|
|
284
|
+
"relative overflow-hidden transition-all duration-300",
|
|
285
|
+
glassmorphism && "bg-background/60 backdrop-blur-md border-white/10",
|
|
286
|
+
isFullscreen && "h-full",
|
|
287
|
+
className
|
|
288
|
+
)}>
|
|
289
|
+
{/* Header */}
|
|
290
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
291
|
+
<div>
|
|
292
|
+
<CardTitle className="text-base font-semibold">{title}</CardTitle>
|
|
293
|
+
{description && (
|
|
294
|
+
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{/* Actions */}
|
|
299
|
+
<div className="flex items-center gap-2">
|
|
300
|
+
{refreshing && (
|
|
301
|
+
<motion.div
|
|
302
|
+
animate={{ rotate: 360 }}
|
|
303
|
+
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
|
304
|
+
>
|
|
305
|
+
<RefreshCw className="h-4 w-4 text-muted-foreground" />
|
|
306
|
+
</motion.div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
{interactive && (
|
|
310
|
+
<DropdownMenu>
|
|
311
|
+
<DropdownMenuTrigger asChild>
|
|
312
|
+
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
313
|
+
<MoreVertical className="h-4 w-4" />
|
|
314
|
+
</Button>
|
|
315
|
+
</DropdownMenuTrigger>
|
|
316
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
317
|
+
<DropdownMenuItem onClick={() => {
|
|
318
|
+
setIsFullscreen(!isFullscreen)
|
|
319
|
+
onAction?.('fullscreen', { fullscreen: !isFullscreen })
|
|
320
|
+
}}>
|
|
321
|
+
<Maximize2 className="mr-2 h-4 w-4" />
|
|
322
|
+
{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
|
|
323
|
+
</DropdownMenuItem>
|
|
324
|
+
|
|
325
|
+
<DropdownMenuSub>
|
|
326
|
+
<DropdownMenuSubTrigger>
|
|
327
|
+
<Palette className="mr-2 h-4 w-4" />
|
|
328
|
+
Color Scheme
|
|
329
|
+
</DropdownMenuSubTrigger>
|
|
330
|
+
<DropdownMenuSubContent>
|
|
331
|
+
{Object.keys(CHART_COLORS).map((scheme) => (
|
|
332
|
+
<DropdownMenuItem
|
|
333
|
+
key={scheme}
|
|
334
|
+
onClick={() => setColorScheme(scheme as keyof typeof CHART_COLORS)}
|
|
335
|
+
>
|
|
336
|
+
<div className="flex items-center gap-2">
|
|
337
|
+
<div className="flex gap-1">
|
|
338
|
+
{CHART_COLORS[scheme as keyof typeof CHART_COLORS].slice(0, 3).map((color, i) => (
|
|
339
|
+
<div
|
|
340
|
+
key={i}
|
|
341
|
+
className="w-3 h-3 rounded-full"
|
|
342
|
+
style={{ backgroundColor: color }}
|
|
343
|
+
/>
|
|
344
|
+
))}
|
|
345
|
+
</div>
|
|
346
|
+
<span className="capitalize">{scheme}</span>
|
|
347
|
+
</div>
|
|
348
|
+
</DropdownMenuItem>
|
|
349
|
+
))}
|
|
350
|
+
</DropdownMenuSubContent>
|
|
351
|
+
</DropdownMenuSub>
|
|
352
|
+
|
|
353
|
+
<DropdownMenuSeparator />
|
|
354
|
+
|
|
355
|
+
<DropdownMenuItem onClick={() => onAction?.('export', { format: 'png' })}>
|
|
356
|
+
<Download className="mr-2 h-4 w-4" />
|
|
357
|
+
Export as PNG
|
|
358
|
+
</DropdownMenuItem>
|
|
359
|
+
|
|
360
|
+
<DropdownMenuItem onClick={() => onAction?.('export', { format: 'csv' })}>
|
|
361
|
+
<Download className="mr-2 h-4 w-4" />
|
|
362
|
+
Export Data
|
|
363
|
+
</DropdownMenuItem>
|
|
364
|
+
|
|
365
|
+
<DropdownMenuSeparator />
|
|
366
|
+
|
|
367
|
+
<DropdownMenuItem onClick={() => onAction?.('filter')}>
|
|
368
|
+
<Filter className="mr-2 h-4 w-4" />
|
|
369
|
+
Filter Data
|
|
370
|
+
</DropdownMenuItem>
|
|
371
|
+
|
|
372
|
+
<DropdownMenuItem onClick={() => onAction?.('settings')}>
|
|
373
|
+
<Settings className="mr-2 h-4 w-4" />
|
|
374
|
+
Chart Settings
|
|
375
|
+
</DropdownMenuItem>
|
|
376
|
+
</DropdownMenuContent>
|
|
377
|
+
</DropdownMenu>
|
|
378
|
+
)}
|
|
379
|
+
</div>
|
|
380
|
+
</CardHeader>
|
|
381
|
+
|
|
382
|
+
{/* Chart Content */}
|
|
383
|
+
<CardContent className="p-0 px-4 pb-4">
|
|
384
|
+
<AnimatePresence mode="wait">
|
|
385
|
+
{loading ? (
|
|
386
|
+
<motion.div
|
|
387
|
+
key="loading"
|
|
388
|
+
initial={{ opacity: 0 }}
|
|
389
|
+
animate={{ opacity: 1 }}
|
|
390
|
+
exit={{ opacity: 0 }}
|
|
391
|
+
className="flex items-center justify-center"
|
|
392
|
+
style={{ height }}
|
|
393
|
+
>
|
|
394
|
+
<div className="space-y-2">
|
|
395
|
+
<div className="animate-pulse bg-muted h-4 w-32 rounded" />
|
|
396
|
+
<div className="animate-pulse bg-muted h-4 w-24 rounded" />
|
|
397
|
+
<div className="animate-pulse bg-muted h-4 w-28 rounded" />
|
|
398
|
+
</div>
|
|
399
|
+
</motion.div>
|
|
400
|
+
) : (
|
|
401
|
+
<motion.div
|
|
402
|
+
key="chart"
|
|
403
|
+
initial={{ opacity: 0, y: 10 }}
|
|
404
|
+
animate={{ opacity: 1, y: 0 }}
|
|
405
|
+
exit={{ opacity: 0, y: -10 }}
|
|
406
|
+
transition={{ duration: 0.3 }}
|
|
407
|
+
>
|
|
408
|
+
{renderChart()}
|
|
409
|
+
</motion.div>
|
|
410
|
+
)}
|
|
411
|
+
</AnimatePresence>
|
|
412
|
+
</CardContent>
|
|
413
|
+
</Card>
|
|
414
|
+
</motion.div>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export default ChartWidget
|