@wakastellar/ui 2.1.0 → 2.1.2

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.
@@ -0,0 +1,424 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils"
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../../components/card"
6
+ import { Button } from "../../components/button"
7
+ import { Badge } from "../../components/badge"
8
+ import { Progress } from "../../components/progress"
9
+ import { Separator } from "../../components/separator"
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from "../../components/select"
17
+ import {
18
+ TrendingUp,
19
+ TrendingDown,
20
+ Users,
21
+ DollarSign,
22
+ ShoppingCart,
23
+ Activity,
24
+ ArrowUpRight,
25
+ ArrowDownRight,
26
+ MoreHorizontal,
27
+ RefreshCw,
28
+ Download,
29
+ Calendar,
30
+ Target,
31
+ Zap,
32
+ Eye,
33
+ Clock,
34
+ } from "lucide-react"
35
+
36
+ // ============================================
37
+ // TYPES
38
+ // ============================================
39
+
40
+ export interface KPIMetric {
41
+ id: string
42
+ label: string
43
+ value: string | number
44
+ previousValue?: string | number
45
+ change?: number
46
+ changeLabel?: string
47
+ trend?: "up" | "down" | "neutral"
48
+ icon?: React.ReactNode
49
+ color?: "default" | "blue" | "green" | "yellow" | "red" | "purple"
50
+ sparkline?: number[]
51
+ target?: number
52
+ current?: number
53
+ }
54
+
55
+ export interface KPIChartData {
56
+ label: string
57
+ value: number
58
+ color?: string
59
+ }
60
+
61
+ export interface DashboardKPIProps {
62
+ /** Title of the dashboard */
63
+ title?: string
64
+ /** Description */
65
+ description?: string
66
+ /** Main KPI metrics */
67
+ metrics: KPIMetric[]
68
+ /** Period selector options */
69
+ periods?: { value: string; label: string }[]
70
+ /** Selected period */
71
+ selectedPeriod?: string
72
+ /** Period change handler */
73
+ onPeriodChange?: (period: string) => void
74
+ /** Refresh handler */
75
+ onRefresh?: () => void
76
+ /** Export handler */
77
+ onExport?: () => void
78
+ /** Custom chart component */
79
+ chart?: React.ReactNode
80
+ /** Secondary metrics for the bottom section */
81
+ secondaryMetrics?: KPIMetric[]
82
+ /** Goals/targets section */
83
+ goals?: {
84
+ id: string
85
+ label: string
86
+ current: number
87
+ target: number
88
+ unit?: string
89
+ }[]
90
+ /** Loading state */
91
+ isLoading?: boolean
92
+ /** Last updated timestamp */
93
+ lastUpdated?: string | Date
94
+ /** Custom className */
95
+ className?: string
96
+ }
97
+
98
+ // ============================================
99
+ // SUBCOMPONENTS
100
+ // ============================================
101
+
102
+ const colorMap = {
103
+ default: "text-foreground",
104
+ blue: "text-blue-500",
105
+ green: "text-green-500",
106
+ yellow: "text-yellow-500",
107
+ red: "text-red-500",
108
+ purple: "text-purple-500",
109
+ }
110
+
111
+ const bgColorMap = {
112
+ default: "bg-muted",
113
+ blue: "bg-blue-500/10",
114
+ green: "bg-green-500/10",
115
+ yellow: "bg-yellow-500/10",
116
+ red: "bg-red-500/10",
117
+ purple: "bg-purple-500/10",
118
+ }
119
+
120
+ function MiniSparkline({ data, color = "blue" }: { data: number[]; color?: string }) {
121
+ const max = Math.max(...data)
122
+ const min = Math.min(...data)
123
+ const range = max - min || 1
124
+
125
+ const points = data
126
+ .map((value, index) => {
127
+ const x = (index / (data.length - 1)) * 100
128
+ const y = 100 - ((value - min) / range) * 100
129
+ return `${x},${y}`
130
+ })
131
+ .join(" ")
132
+
133
+ return (
134
+ <svg className="w-16 h-8" viewBox="0 0 100 100" preserveAspectRatio="none">
135
+ <polyline
136
+ points={points}
137
+ fill="none"
138
+ stroke="currentColor"
139
+ strokeWidth="3"
140
+ className={cn("opacity-60", colorMap[color as keyof typeof colorMap] || colorMap.blue)}
141
+ />
142
+ </svg>
143
+ )
144
+ }
145
+
146
+ function KPICard({ metric }: { metric: KPIMetric }) {
147
+ const Icon = metric.icon
148
+ const trendColor = metric.trend === "up" ? "text-green-500" : metric.trend === "down" ? "text-red-500" : "text-muted-foreground"
149
+ const TrendIcon = metric.trend === "up" ? TrendingUp : metric.trend === "down" ? TrendingDown : null
150
+
151
+ return (
152
+ <Card className="relative overflow-hidden">
153
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
154
+ <CardTitle className="text-sm font-medium text-muted-foreground">
155
+ {metric.label}
156
+ </CardTitle>
157
+ {Icon && (
158
+ <div className={cn("p-2 rounded-lg", bgColorMap[metric.color || "default"])}>
159
+ <span className={cn("h-4 w-4", colorMap[metric.color || "default"])}>
160
+ {Icon}
161
+ </span>
162
+ </div>
163
+ )}
164
+ </CardHeader>
165
+ <CardContent>
166
+ <div className="flex items-end justify-between">
167
+ <div>
168
+ <div className="text-2xl font-bold">{metric.value}</div>
169
+ {metric.change !== undefined && (
170
+ <div className={cn("flex items-center gap-1 text-sm", trendColor)}>
171
+ {TrendIcon && <TrendIcon className="h-3 w-3" />}
172
+ <span>{metric.change > 0 ? "+" : ""}{metric.change}%</span>
173
+ {metric.changeLabel && (
174
+ <span className="text-muted-foreground">{metric.changeLabel}</span>
175
+ )}
176
+ </div>
177
+ )}
178
+ </div>
179
+ {metric.sparkline && (
180
+ <MiniSparkline data={metric.sparkline} color={metric.color} />
181
+ )}
182
+ </div>
183
+ {metric.target !== undefined && metric.current !== undefined && (
184
+ <div className="mt-4">
185
+ <div className="flex justify-between text-xs text-muted-foreground mb-1">
186
+ <span>Progress</span>
187
+ <span>{Math.round((metric.current / metric.target) * 100)}%</span>
188
+ </div>
189
+ <Progress value={(metric.current / metric.target) * 100} className="h-1.5" />
190
+ </div>
191
+ )}
192
+ </CardContent>
193
+ </Card>
194
+ )
195
+ }
196
+
197
+ function GoalCard({ goal }: { goal: { id: string; label: string; current: number; target: number; unit?: string } }) {
198
+ const progress = (goal.current / goal.target) * 100
199
+ const isComplete = progress >= 100
200
+
201
+ return (
202
+ <div className="flex items-center gap-4 p-4 rounded-lg border bg-card">
203
+ <div className={cn(
204
+ "h-10 w-10 rounded-full flex items-center justify-center",
205
+ isComplete ? "bg-green-500/10 text-green-500" : "bg-muted text-muted-foreground"
206
+ )}>
207
+ <Target className="h-5 w-5" />
208
+ </div>
209
+ <div className="flex-1 min-w-0">
210
+ <div className="flex items-center justify-between mb-1">
211
+ <span className="font-medium truncate">{goal.label}</span>
212
+ <span className="text-sm text-muted-foreground">
213
+ {goal.current}{goal.unit} / {goal.target}{goal.unit}
214
+ </span>
215
+ </div>
216
+ <Progress value={Math.min(progress, 100)} className="h-2" />
217
+ </div>
218
+ {isComplete && (
219
+ <Badge variant="default" className="bg-green-500">Complete</Badge>
220
+ )}
221
+ </div>
222
+ )
223
+ }
224
+
225
+ // ============================================
226
+ // MAIN COMPONENT
227
+ // ============================================
228
+
229
+ export function DashboardKPI({
230
+ title = "Dashboard",
231
+ description,
232
+ metrics,
233
+ periods = [
234
+ { value: "7d", label: "7 days" },
235
+ { value: "30d", label: "30 days" },
236
+ { value: "90d", label: "90 days" },
237
+ { value: "12m", label: "12 months" },
238
+ ],
239
+ selectedPeriod = "30d",
240
+ onPeriodChange,
241
+ onRefresh,
242
+ onExport,
243
+ chart,
244
+ secondaryMetrics,
245
+ goals,
246
+ isLoading = false,
247
+ lastUpdated,
248
+ className,
249
+ }: DashboardKPIProps) {
250
+ return (
251
+ <div className={cn("space-y-6", className)}>
252
+ {/* Header */}
253
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
254
+ <div>
255
+ <h1 className="text-2xl font-bold tracking-tight">{title}</h1>
256
+ {description && (
257
+ <p className="text-muted-foreground">{description}</p>
258
+ )}
259
+ </div>
260
+ <div className="flex items-center gap-2">
261
+ {periods.length > 0 && (
262
+ <Select value={selectedPeriod} onValueChange={onPeriodChange}>
263
+ <SelectTrigger className="w-[140px]">
264
+ <Calendar className="h-4 w-4 mr-2" />
265
+ <SelectValue />
266
+ </SelectTrigger>
267
+ <SelectContent>
268
+ {periods.map((period) => (
269
+ <SelectItem key={period.value} value={period.value}>
270
+ {period.label}
271
+ </SelectItem>
272
+ ))}
273
+ </SelectContent>
274
+ </Select>
275
+ )}
276
+ {onRefresh && (
277
+ <Button variant="outline" size="icon" onClick={onRefresh} disabled={isLoading}>
278
+ <RefreshCw className={cn("h-4 w-4", isLoading && "animate-spin")} />
279
+ </Button>
280
+ )}
281
+ {onExport && (
282
+ <Button variant="outline" size="icon" onClick={onExport}>
283
+ <Download className="h-4 w-4" />
284
+ </Button>
285
+ )}
286
+ </div>
287
+ </div>
288
+
289
+ {/* Last Updated */}
290
+ {lastUpdated && (
291
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
292
+ <Clock className="h-4 w-4" />
293
+ <span>Last updated: {typeof lastUpdated === "string" ? lastUpdated : lastUpdated.toLocaleString()}</span>
294
+ </div>
295
+ )}
296
+
297
+ {/* Main KPI Grid */}
298
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
299
+ {metrics.slice(0, 4).map((metric) => (
300
+ <KPICard key={metric.id} metric={metric} />
301
+ ))}
302
+ </div>
303
+
304
+ {/* Chart Section */}
305
+ {chart && (
306
+ <Card>
307
+ <CardHeader>
308
+ <CardTitle>Overview</CardTitle>
309
+ <CardDescription>Performance metrics over time</CardDescription>
310
+ </CardHeader>
311
+ <CardContent>{chart}</CardContent>
312
+ </Card>
313
+ )}
314
+
315
+ {/* Secondary Metrics */}
316
+ {secondaryMetrics && secondaryMetrics.length > 0 && (
317
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
318
+ {secondaryMetrics.map((metric) => (
319
+ <Card key={metric.id}>
320
+ <CardContent className="pt-6">
321
+ <div className="flex items-center justify-between">
322
+ <div>
323
+ <p className="text-sm text-muted-foreground">{metric.label}</p>
324
+ <p className="text-xl font-bold">{metric.value}</p>
325
+ </div>
326
+ {metric.change !== undefined && (
327
+ <Badge
328
+ variant={metric.trend === "up" ? "default" : metric.trend === "down" ? "destructive" : "secondary"}
329
+ className="gap-1"
330
+ >
331
+ {metric.trend === "up" ? (
332
+ <ArrowUpRight className="h-3 w-3" />
333
+ ) : metric.trend === "down" ? (
334
+ <ArrowDownRight className="h-3 w-3" />
335
+ ) : null}
336
+ {metric.change > 0 ? "+" : ""}{metric.change}%
337
+ </Badge>
338
+ )}
339
+ </div>
340
+ </CardContent>
341
+ </Card>
342
+ ))}
343
+ </div>
344
+ )}
345
+
346
+ {/* Goals Section */}
347
+ {goals && goals.length > 0 && (
348
+ <Card>
349
+ <CardHeader>
350
+ <CardTitle className="flex items-center gap-2">
351
+ <Target className="h-5 w-5" />
352
+ Goals & Targets
353
+ </CardTitle>
354
+ <CardDescription>Track your progress towards key objectives</CardDescription>
355
+ </CardHeader>
356
+ <CardContent className="space-y-4">
357
+ {goals.map((goal) => (
358
+ <GoalCard key={goal.id} goal={goal} />
359
+ ))}
360
+ </CardContent>
361
+ </Card>
362
+ )}
363
+ </div>
364
+ )
365
+ }
366
+
367
+ // ============================================
368
+ // PRESET DATA
369
+ // ============================================
370
+
371
+ export const defaultKPIMetrics: KPIMetric[] = [
372
+ {
373
+ id: "revenue",
374
+ label: "Total Revenue",
375
+ value: "$45,231.89",
376
+ change: 20.1,
377
+ changeLabel: "from last month",
378
+ trend: "up",
379
+ icon: <DollarSign className="h-4 w-4" />,
380
+ color: "green",
381
+ sparkline: [30, 40, 35, 50, 49, 60, 70, 91, 125],
382
+ },
383
+ {
384
+ id: "subscriptions",
385
+ label: "Subscriptions",
386
+ value: "+2,350",
387
+ change: 180.1,
388
+ changeLabel: "from last month",
389
+ trend: "up",
390
+ icon: <Users className="h-4 w-4" />,
391
+ color: "blue",
392
+ sparkline: [10, 20, 30, 40, 50, 60, 70, 80, 90],
393
+ },
394
+ {
395
+ id: "sales",
396
+ label: "Sales",
397
+ value: "+12,234",
398
+ change: 19,
399
+ changeLabel: "from last month",
400
+ trend: "up",
401
+ icon: <ShoppingCart className="h-4 w-4" />,
402
+ color: "purple",
403
+ sparkline: [65, 59, 80, 81, 56, 55, 70, 80, 95],
404
+ },
405
+ {
406
+ id: "active",
407
+ label: "Active Now",
408
+ value: "+573",
409
+ change: -2.5,
410
+ changeLabel: "from last hour",
411
+ trend: "down",
412
+ icon: <Activity className="h-4 w-4" />,
413
+ color: "yellow",
414
+ sparkline: [100, 90, 85, 88, 70, 65, 60, 58, 55],
415
+ },
416
+ ]
417
+
418
+ export const defaultGoals = [
419
+ { id: "revenue-goal", label: "Monthly Revenue Target", current: 45231, target: 50000, unit: "$" },
420
+ { id: "users-goal", label: "New User Signups", current: 2350, target: 2000, unit: "" },
421
+ { id: "conversion-goal", label: "Conversion Rate", current: 3.2, target: 5, unit: "%" },
422
+ ]
423
+
424
+ export default DashboardKPI