@turtleclub/ui 0.0.1

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 (50) hide show
  1. package/.turbo/turbo-build.log +15 -0
  2. package/.turbo/turbo-type-check.log +360 -0
  3. package/README.md +3 -0
  4. package/components.json +21 -0
  5. package/dist/index.cjs +2 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.js +1672 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/styles.css +1 -0
  10. package/package.json +66 -0
  11. package/src/components/molecules/index.ts +7 -0
  12. package/src/components/molecules/opportunity-details.tsx +145 -0
  13. package/src/components/molecules/opportunity-item.tsx +63 -0
  14. package/src/components/molecules/route-details.tsx +87 -0
  15. package/src/components/molecules/swap-details.tsx +95 -0
  16. package/src/components/molecules/swap-input.tsx +115 -0
  17. package/src/components/molecules/token-selector.tsx +72 -0
  18. package/src/components/molecules/tx-status.tsx +254 -0
  19. package/src/components/ui/button.tsx +65 -0
  20. package/src/components/ui/card.tsx +101 -0
  21. package/src/components/ui/chip.tsx +48 -0
  22. package/src/components/ui/icon-animation.tsx +82 -0
  23. package/src/components/ui/index.ts +18 -0
  24. package/src/components/ui/info-card.tsx +128 -0
  25. package/src/components/ui/input.tsx +78 -0
  26. package/src/components/ui/label-with-icon.tsx +112 -0
  27. package/src/components/ui/label.tsx +22 -0
  28. package/src/components/ui/navigation-bar.tsx +135 -0
  29. package/src/components/ui/opportunity-details-v1.tsx +90 -0
  30. package/src/components/ui/scroll-area.tsx +56 -0
  31. package/src/components/ui/select.tsx +180 -0
  32. package/src/components/ui/separator.tsx +26 -0
  33. package/src/components/ui/sonner.tsx +23 -0
  34. package/src/components/ui/switch.tsx +29 -0
  35. package/src/components/ui/toggle-group.tsx +71 -0
  36. package/src/components/ui/toggle.tsx +47 -0
  37. package/src/components/ui/tooltip.tsx +59 -0
  38. package/src/index.ts +9 -0
  39. package/src/lib/utils.ts +6 -0
  40. package/src/styles/globals.css +75 -0
  41. package/src/styles/themes/index.css +9 -0
  42. package/src/styles/themes/semantic.css +107 -0
  43. package/src/styles/tokens/colors.css +77 -0
  44. package/src/styles/tokens/index.css +15 -0
  45. package/src/styles/tokens/radius.css +46 -0
  46. package/src/styles/tokens/spacing.css +52 -0
  47. package/src/styles/tokens/typography.css +86 -0
  48. package/src/tokens/index.ts +108 -0
  49. package/tsconfig.json +21 -0
  50. package/vite.config.js +65 -0
@@ -0,0 +1,254 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { Card } from "@/components/ui/card";
4
+ import { IconAnimation } from "@/components/ui/icon-animation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { CheckIcon, TurtleIcon, XIcon } from "lucide-react";
7
+
8
+ interface TxStatusProps extends React.HTMLAttributes<HTMLDivElement> {
9
+ title?: string;
10
+ description?: string;
11
+ txHash?: string;
12
+ explorerUrl?: string;
13
+ estimatedTime?: string;
14
+ amount?: string;
15
+ token?: string;
16
+ protocol?: string;
17
+ steps?: Array<{
18
+ label: string;
19
+ completed: boolean;
20
+ current?: boolean;
21
+ txHash?: string;
22
+ }>;
23
+ variant?: "default" | "compact";
24
+ completed?: boolean;
25
+ cancelled?: boolean;
26
+ onViewDetails?: () => void;
27
+ onClose?: () => void;
28
+ showActions?: boolean;
29
+ }
30
+
31
+ const TxStatus = React.forwardRef<HTMLDivElement, TxStatusProps>(
32
+ (
33
+ {
34
+ className,
35
+ title,
36
+ description,
37
+ txHash,
38
+ explorerUrl,
39
+ estimatedTime,
40
+ amount,
41
+ token,
42
+ protocol,
43
+ steps = [],
44
+ variant = "default",
45
+ completed = false,
46
+ cancelled = false,
47
+ onViewDetails,
48
+ onClose,
49
+ showActions = true,
50
+ ...props
51
+ },
52
+ ref
53
+ ) => {
54
+ const defaultTitle = cancelled
55
+ ? "Transaction Cancelled"
56
+ : completed
57
+ ? "Transaction Completed"
58
+ : "Processing Transaction";
59
+ const defaultDescription = cancelled
60
+ ? "Transaction was cancelled by user"
61
+ : completed
62
+ ? "Your transaction has been successfully processed!"
63
+ : "Please wait while your transaction is being processed...";
64
+
65
+ const ExternalLinkIcon = ({ size = "w-4 h-4" }) => (
66
+ <svg className={size} fill="none" stroke="currentColor" viewBox="0 0 24 24">
67
+ <path
68
+ strokeLinecap="round"
69
+ strokeLinejoin="round"
70
+ strokeWidth={2}
71
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
72
+ />
73
+ </svg>
74
+ );
75
+
76
+ const StepCheckIcon = () => (
77
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
79
+ </svg>
80
+ );
81
+
82
+ const formatTxHash = (hash: string) => {
83
+ if (hash.length <= 12) return hash;
84
+ return `${hash.slice(0, 6)}...${hash.slice(-6)}`;
85
+ };
86
+
87
+ return (
88
+ <Card
89
+ ref={ref}
90
+ className={cn(
91
+ "p-6 text-center space-y-4",
92
+ variant === "compact" && "p-4 space-y-3",
93
+ className
94
+ )}
95
+ {...props}
96
+ >
97
+ {/* Animation */}
98
+ <div className="flex justify-center">
99
+ <IconAnimation spinning={!completed && !cancelled} size={variant === "compact" ? "default" : "lg"}>
100
+ {cancelled ? (
101
+ <XIcon className="w-8 h-8 text-destructive" />
102
+ ) : completed ? (
103
+ <CheckIcon className="w-8 h-8 text-primary" />
104
+ ) : (
105
+ <TurtleIcon className="w-8 h-8 text-primary" />
106
+ )}
107
+ </IconAnimation>
108
+ </div>
109
+
110
+ {/* Title and description */}
111
+ <div className="space-y-2">
112
+ <h3
113
+ className={cn(
114
+ "font-medium text-foreground",
115
+ variant === "compact" ? "text-sm" : "text-base"
116
+ )}
117
+ >
118
+ {title || defaultTitle}
119
+ </h3>
120
+ <p className={cn("text-muted-foreground", variant === "compact" ? "text-xs" : "text-sm")}>
121
+ {description || defaultDescription}
122
+ </p>
123
+ </div>
124
+
125
+ {/* Transaction summary (completed state) */}
126
+ {completed && !cancelled && (amount || token || protocol) && (
127
+ <div className="p-3 bg-muted/50 rounded-lg space-y-1">
128
+ {amount && token && (
129
+ <div className="text-sm font-medium">
130
+ {amount} {token}
131
+ </div>
132
+ )}
133
+ {protocol && <div className="text-xs text-muted-foreground">via {protocol}</div>}
134
+ </div>
135
+ )}
136
+
137
+ {/* Transaction hash link */}
138
+ {txHash && !cancelled && (
139
+ <div className="space-y-1">
140
+ <div className="text-xs text-muted-foreground">Transaction Hash</div>
141
+ {explorerUrl ? (
142
+ <a
143
+ href={`${explorerUrl}/tx/${txHash}`}
144
+ target="_blank"
145
+ rel="noopener noreferrer"
146
+ className="inline-flex items-center gap-2 text-sm text-primary hover:text-primary/80 transition-colors"
147
+ >
148
+ {formatTxHash(txHash)}
149
+ <ExternalLinkIcon />
150
+ </a>
151
+ ) : (
152
+ <div className="text-sm font-mono text-foreground">{formatTxHash(txHash)}</div>
153
+ )}
154
+ </div>
155
+ )}
156
+
157
+ {/* Estimated time (pending state) */}
158
+ {!completed && !cancelled && estimatedTime && (
159
+ <div className="text-xs text-muted-foreground">Estimated time: {estimatedTime}</div>
160
+ )}
161
+
162
+ {/* Progress steps (pending state) */}
163
+ {!completed && !cancelled && steps.length > 0 && (
164
+ <div className="space-y-2 pt-2 border-t border-border">
165
+ <div className="text-xs font-medium text-muted-foreground text-left">Progress</div>
166
+ <div className="space-y-2">
167
+ {steps.map((step, index) => (
168
+ <div key={index} className="flex items-center gap-3 text-left">
169
+ <div
170
+ className={cn(
171
+ "flex items-center justify-center w-5 h-5 rounded-full border-2 flex-shrink-0",
172
+ step.completed && "bg-primary border-primary text-primary-foreground",
173
+ step.current &&
174
+ !step.completed &&
175
+ "border-primary text-primary animate-pulse",
176
+ !step.completed &&
177
+ !step.current &&
178
+ "border-muted-foreground/30 text-muted-foreground"
179
+ )}
180
+ >
181
+ {step.completed ? (
182
+ <StepCheckIcon />
183
+ ) : step.current ? (
184
+ <div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
185
+ ) : (
186
+ <div className="w-2 h-2 rounded-full bg-muted-foreground/30" />
187
+ )}
188
+ </div>
189
+ <div className="flex-1 flex items-center justify-between">
190
+ <span
191
+ className={cn(
192
+ "text-sm",
193
+ step.completed && "text-foreground",
194
+ step.current && !step.completed && "text-foreground font-medium",
195
+ !step.completed && !step.current && "text-muted-foreground"
196
+ )}
197
+ >
198
+ {step.label}
199
+ </span>
200
+ {step.txHash && explorerUrl && (
201
+ <a
202
+ href={`${explorerUrl}/tx/${step.txHash}`}
203
+ target="_blank"
204
+ rel="noopener noreferrer"
205
+ className="inline-flex items-center gap-1 text-xs text-primary hover:text-primary/80 transition-colors ml-2"
206
+ title={`View transaction: ${step.txHash}`}
207
+ >
208
+ <span className="font-mono">{step.txHash.slice(0, 6)}...{step.txHash.slice(-4)}</span>
209
+ <ExternalLinkIcon size="w-3 h-3" />
210
+ </a>
211
+ )}
212
+ </div>
213
+ </div>
214
+ ))}
215
+ </div>
216
+ </div>
217
+ )}
218
+
219
+ {/* Action buttons (completed or cancelled state) */}
220
+ {(completed || cancelled) && showActions && (onViewDetails || onClose) && (
221
+ <div
222
+ className={cn(
223
+ "flex gap-2 pt-2",
224
+ variant === "compact" ? "flex-col" : "flex-row justify-center"
225
+ )}
226
+ >
227
+ {!cancelled && onViewDetails && (
228
+ <Button
229
+ variant="transparentWhite"
230
+ size={variant === "compact" ? "sm" : "default"}
231
+ onClick={onViewDetails}
232
+ >
233
+ View Details
234
+ </Button>
235
+ )}
236
+ {onClose && (
237
+ <Button
238
+ variant="default"
239
+ size={variant === "compact" ? "sm" : "default"}
240
+ onClick={onClose}
241
+ >
242
+ {cancelled ? "Try Again" : "Done"}
243
+ </Button>
244
+ )}
245
+ </div>
246
+ )}
247
+ </Card>
248
+ );
249
+ }
250
+ );
251
+
252
+ TxStatus.displayName = "TxStatus";
253
+
254
+ export { TxStatus };
@@ -0,0 +1,65 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none border",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ // Turtle Design System variants using semantic classes
13
+ default:
14
+ "bg-background text-primary border-primary shadow-[0_0_4px_0_hsl(var(--primary))] hover:bg-background/90 hover:shadow-[0_0_6px_0_hsl(var(--primary))]",
15
+ green:
16
+ "bg-primary text-primary-foreground border-primary shadow-[0_0_4px_0_hsl(var(--primary))] hover:bg-primary/90 hover:shadow-[0_0_6px_0_hsl(var(--primary))]",
17
+ transparentWhite: "bg-secondary text-secondary-foreground border-secondary hover:bg-secondary/80",
18
+ transparentGreen: "bg-secondary text-primary border-secondary hover:bg-secondary/80",
19
+ },
20
+ size: {
21
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
22
+ sm: "h-8 gap-1.5 px-3 has-[>svg]:px-2.5",
23
+ lg: "h-10 px-6 has-[>svg]:px-4",
24
+ icon: "size-9",
25
+ },
26
+ rounded: {
27
+ default: "rounded-full",
28
+ none: "rounded-none",
29
+ sm: "rounded-sm",
30
+ md: "rounded-md",
31
+ lg: "rounded-lg",
32
+ full: "rounded-full",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: "default",
37
+ size: "default",
38
+ rounded: "default",
39
+ },
40
+ }
41
+ );
42
+
43
+ function Button({
44
+ className,
45
+ variant,
46
+ size,
47
+ rounded,
48
+ asChild = false,
49
+ ...props
50
+ }: React.ComponentProps<"button"> &
51
+ VariantProps<typeof buttonVariants> & {
52
+ asChild?: boolean;
53
+ }) {
54
+ const Comp = asChild ? Slot : "button";
55
+
56
+ return (
57
+ <Comp
58
+ data-slot="button"
59
+ className={cn(buttonVariants({ variant, size, rounded, className }))}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ export { Button, buttonVariants };
@@ -0,0 +1,101 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const cardVariants = cva("transition-all", {
7
+ variants: {
8
+ variant: {
9
+ // Turtle Design System variants
10
+ container:
11
+ "relative bg-card shadow-[0_0_20px_0_rgba(0,0,0,0.25)] before:absolute before:inset-0 before:rounded-[inherit] before:p-px before:bg-gradient-to-b before:from-[#F9F9F9] before:from-40% before:via-white before:via-40.01% before:to-white before:to-50% before:content-[''] before:pointer-events-none before:-z-10",
12
+ simple: "bg-muted",
13
+ item: "bg-muted shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]",
14
+ },
15
+ padding: {
16
+ none: "p-0",
17
+ sm: "p-3",
18
+ default: "p-4",
19
+ md: "p-6",
20
+ lg: "p-8",
21
+ },
22
+ rounded: {
23
+ default: "rounded-lg",
24
+ none: "rounded-none",
25
+ sm: "rounded-sm",
26
+ md: "rounded-md",
27
+ lg: "rounded-lg",
28
+ xl: "rounded-xl",
29
+ full: "rounded-full",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "container",
34
+ padding: "default",
35
+ rounded: "default",
36
+ },
37
+ });
38
+
39
+ function Card({
40
+ className,
41
+ variant,
42
+ padding,
43
+ rounded,
44
+ ...props
45
+ }: React.ComponentProps<"div"> & VariantProps<typeof cardVariants>) {
46
+ return (
47
+ <div
48
+ data-slot="card"
49
+ className={cn(cardVariants({ variant, padding, rounded, className }))}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
56
+ return (
57
+ <div
58
+ data-slot="card-header"
59
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ function CardTitle({ className, children, ...props }: React.ComponentProps<"h3">) {
66
+ return (
67
+ <h3
68
+ data-slot="card-title"
69
+ className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
70
+ {...props}
71
+ >
72
+ {children}
73
+ </h3>
74
+ );
75
+ }
76
+
77
+ function CardDescription({ className, ...props }: React.ComponentProps<"p">) {
78
+ return (
79
+ <p
80
+ data-slot="card-description"
81
+ className={cn("text-sm text-muted-foreground", className)}
82
+ {...props}
83
+ />
84
+ );
85
+ }
86
+
87
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
88
+ return <div data-slot="card-content" className={cn("p-6 pt-0", className)} {...props} />;
89
+ }
90
+
91
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
92
+ return (
93
+ <div
94
+ data-slot="card-footer"
95
+ className={cn("flex items-center p-6 pt-0", className)}
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, cardVariants };
@@ -0,0 +1,48 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const chipVariants = cva(
7
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 cursor-pointer border-none rounded-full",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
12
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
13
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
14
+ },
15
+ size: {
16
+ xs: "h-6 px-2 py-1 text-xs",
17
+ default: "h-8 px-4 py-1",
18
+ sm: "h-7 px-3 py-1 text-xs",
19
+ lg: "h-9 px-6 py-2",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "default",
25
+ },
26
+ }
27
+ );
28
+
29
+ export interface ChipProps extends React.ComponentProps<"div">, VariantProps<typeof chipVariants> {
30
+ asChild?: boolean;
31
+ }
32
+
33
+ const Chip = React.forwardRef<HTMLDivElement, ChipProps>(
34
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
35
+ return (
36
+ <div
37
+ data-slot="chip"
38
+ className={cn(chipVariants({ variant, size, className }))}
39
+ ref={ref}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+ );
45
+
46
+ Chip.displayName = "Chip";
47
+
48
+ export { Chip, chipVariants };
@@ -0,0 +1,82 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const iconAnimationVariants = cva(
7
+ "relative inline-flex items-center justify-center rounded-full",
8
+ {
9
+ variants: {
10
+ size: {
11
+ default: "w-12 h-12",
12
+ sm: "w-8 h-8",
13
+ lg: "w-16 h-16",
14
+ xl: "w-20 h-20",
15
+ },
16
+ variant: {
17
+ default: "bg-background",
18
+ transparent: "bg-transparent",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ size: "default",
23
+ variant: "default",
24
+ },
25
+ }
26
+ );
27
+
28
+ export interface IconAnimationProps
29
+ extends React.ComponentProps<"div">,
30
+ VariantProps<typeof iconAnimationVariants> {
31
+ children: React.ReactNode;
32
+ spinning?: boolean;
33
+ }
34
+
35
+ const IconAnimation = React.forwardRef<HTMLDivElement, IconAnimationProps>(
36
+ ({ className, size, variant, children, spinning = true, ...props }, ref) => {
37
+ return (
38
+ <div
39
+ ref={ref}
40
+ className={cn(iconAnimationVariants({ size, variant }), className)}
41
+ {...props}
42
+ >
43
+ {/* Animated border */}
44
+ <div
45
+ className={cn(
46
+ "absolute inset-0 rounded-full",
47
+ "bg-gradient-to-r from-transparent via-primary to-transparent",
48
+ "animate-spin",
49
+ {
50
+ "animate-spin": spinning,
51
+ "animate-none": !spinning,
52
+ }
53
+ )}
54
+ style={{
55
+ background: spinning
56
+ ? "conic-gradient(from 0deg, transparent 0deg, var(--primary) 180deg, transparent 360deg)"
57
+ : "conic-gradient(from 0deg, var(--primary) 0deg, var(--primary) 360deg)",
58
+ padding: "1px",
59
+ borderRadius: "9999px",
60
+ }}
61
+ >
62
+ {/* Inner circle to create the border effect */}
63
+ <div
64
+ className={cn(
65
+ "w-full h-full rounded-full",
66
+ variant === "transparent" ? "bg-transparent" : "bg-background"
67
+ )}
68
+ />
69
+ </div>
70
+
71
+ {/* Icon container - stays fixed */}
72
+ <div className="relative z-10 flex items-center justify-center">
73
+ {children}
74
+ </div>
75
+ </div>
76
+ );
77
+ }
78
+ );
79
+
80
+ IconAnimation.displayName = "IconAnimation";
81
+
82
+ export { IconAnimation, iconAnimationVariants };
@@ -0,0 +1,18 @@
1
+ export * from "./input";
2
+ export * from "./label";
3
+ export * from "./scroll-area";
4
+ export * from "./select";
5
+ export * from "./separator";
6
+ export * from "./sonner";
7
+ export * from "./switch";
8
+ export * from "./toggle-group";
9
+ export * from "./toggle";
10
+ export * from "./tooltip";
11
+ export * from "./button";
12
+ export * from "./card";
13
+ export * from "./chip";
14
+ export * from "./icon-animation";
15
+ export * from "./label-with-icon";
16
+ export * from "./navigation-bar";
17
+ export * from "./info-card";
18
+ export * from "./opportunity-details-v1";
@@ -0,0 +1,128 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { Card } from "./card";
4
+ import type { VariantProps } from "class-variance-authority";
5
+ import { cva } from "class-variance-authority";
6
+
7
+ const infoCardVariants = cva("space-y-1", {
8
+ variants: {
9
+ size: {
10
+ sm: "p-2",
11
+ default: "p-3",
12
+ lg: "p-4",
13
+ },
14
+ align: {
15
+ left: "text-left",
16
+ center: "text-center",
17
+ right: "text-right",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ size: "default",
22
+ align: "center",
23
+ },
24
+ });
25
+
26
+ const valueVariants = cva("font-bold", {
27
+ variants: {
28
+ color: {
29
+ primary: "text-foreground",
30
+ secondary: "text-muted-foreground",
31
+ accent: "text-primary",
32
+ success: "text-green-600",
33
+ warning: "text-yellow-600",
34
+ error: "text-red-600",
35
+ },
36
+ size: {
37
+ sm: "text-sm",
38
+ default: "text-lg",
39
+ lg: "text-xl",
40
+ xl: "text-2xl",
41
+ },
42
+ },
43
+ defaultVariants: {
44
+ color: "primary",
45
+ size: "default",
46
+ },
47
+ });
48
+
49
+ const titleVariants = cva("font-medium text-muted-foreground", {
50
+ variants: {
51
+ size: {
52
+ sm: "text-xs",
53
+ default: "text-xs",
54
+ lg: "text-sm",
55
+ },
56
+ },
57
+ defaultVariants: {
58
+ size: "default",
59
+ },
60
+ });
61
+
62
+ interface InfoCardProps
63
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "color">,
64
+ VariantProps<typeof infoCardVariants> {
65
+ title: string;
66
+ value: string;
67
+ color?: VariantProps<typeof valueVariants>["color"];
68
+ valueSize?: VariantProps<typeof valueVariants>["size"];
69
+ titleSize?: VariantProps<typeof titleVariants>["size"];
70
+ align?: VariantProps<typeof infoCardVariants>["align"];
71
+ icon?: React.ReactNode;
72
+ subtitle?: string;
73
+ }
74
+
75
+ const InfoCard = React.forwardRef<HTMLDivElement, InfoCardProps>(
76
+ (
77
+ {
78
+ title,
79
+ value,
80
+ color = "primary",
81
+ valueSize = "default",
82
+ titleSize = "default",
83
+ size = "default",
84
+ align = "center",
85
+ icon,
86
+ subtitle,
87
+ className,
88
+ ...props
89
+ },
90
+ ref
91
+ ) => {
92
+ return (
93
+ <Card
94
+ ref={ref}
95
+ variant="item"
96
+ className={cn(infoCardVariants({ size, align }), className)}
97
+ {...props}
98
+ >
99
+ {/* Icon */}
100
+ {icon && (
101
+ <div
102
+ className={cn(
103
+ "flex mb-2",
104
+ align === "left" && "justify-start",
105
+ align === "center" && "justify-center",
106
+ align === "right" && "justify-end"
107
+ )}
108
+ >
109
+ {icon}
110
+ </div>
111
+ )}
112
+
113
+ {/* Title */}
114
+ <p className={titleVariants({ size: titleSize })}>{title}</p>
115
+
116
+ {/* Value */}
117
+ <p className={valueVariants({ color, size: valueSize })}>{value}</p>
118
+
119
+ {/* Subtitle */}
120
+ {subtitle && <p className="text-xs text-muted-foreground/70">{subtitle}</p>}
121
+ </Card>
122
+ );
123
+ }
124
+ );
125
+
126
+ InfoCard.displayName = "InfoCard";
127
+
128
+ export { InfoCard };