@castlekit/castle 0.0.1 → 0.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 (63) hide show
  1. package/README.md +38 -1
  2. package/bin/castle.js +94 -0
  3. package/install.sh +722 -0
  4. package/next.config.ts +7 -0
  5. package/package.json +54 -5
  6. package/postcss.config.mjs +7 -0
  7. package/src/app/api/avatars/[id]/route.ts +75 -0
  8. package/src/app/api/openclaw/agents/route.ts +107 -0
  9. package/src/app/api/openclaw/config/route.ts +94 -0
  10. package/src/app/api/openclaw/events/route.ts +96 -0
  11. package/src/app/api/openclaw/logs/route.ts +59 -0
  12. package/src/app/api/openclaw/ping/route.ts +68 -0
  13. package/src/app/api/openclaw/restart/route.ts +65 -0
  14. package/src/app/api/openclaw/sessions/route.ts +62 -0
  15. package/src/app/globals.css +286 -0
  16. package/src/app/icon.png +0 -0
  17. package/src/app/layout.tsx +42 -0
  18. package/src/app/page.tsx +269 -0
  19. package/src/app/ui-kit/page.tsx +684 -0
  20. package/src/cli/onboarding.ts +576 -0
  21. package/src/components/dashboard/agent-status.tsx +107 -0
  22. package/src/components/dashboard/glass-card.tsx +28 -0
  23. package/src/components/dashboard/goal-widget.tsx +174 -0
  24. package/src/components/dashboard/greeting-widget.tsx +78 -0
  25. package/src/components/dashboard/index.ts +7 -0
  26. package/src/components/dashboard/stat-widget.tsx +61 -0
  27. package/src/components/dashboard/stock-widget.tsx +164 -0
  28. package/src/components/dashboard/weather-widget.tsx +68 -0
  29. package/src/components/icons/castle-icon.tsx +21 -0
  30. package/src/components/kanban/index.ts +3 -0
  31. package/src/components/kanban/kanban-board.tsx +391 -0
  32. package/src/components/kanban/kanban-card.tsx +137 -0
  33. package/src/components/kanban/kanban-column.tsx +98 -0
  34. package/src/components/layout/index.ts +4 -0
  35. package/src/components/layout/page-header.tsx +20 -0
  36. package/src/components/layout/sidebar.tsx +128 -0
  37. package/src/components/layout/theme-toggle.tsx +59 -0
  38. package/src/components/layout/user-menu.tsx +72 -0
  39. package/src/components/ui/alert.tsx +72 -0
  40. package/src/components/ui/avatar.tsx +87 -0
  41. package/src/components/ui/badge.tsx +39 -0
  42. package/src/components/ui/button.tsx +43 -0
  43. package/src/components/ui/card.tsx +107 -0
  44. package/src/components/ui/checkbox.tsx +56 -0
  45. package/src/components/ui/clock.tsx +171 -0
  46. package/src/components/ui/dialog.tsx +105 -0
  47. package/src/components/ui/index.ts +34 -0
  48. package/src/components/ui/input.tsx +112 -0
  49. package/src/components/ui/option-card.tsx +151 -0
  50. package/src/components/ui/progress.tsx +103 -0
  51. package/src/components/ui/radio.tsx +109 -0
  52. package/src/components/ui/select.tsx +46 -0
  53. package/src/components/ui/slider.tsx +62 -0
  54. package/src/components/ui/tabs.tsx +132 -0
  55. package/src/components/ui/toggle-group.tsx +85 -0
  56. package/src/components/ui/toggle.tsx +78 -0
  57. package/src/components/ui/tooltip.tsx +145 -0
  58. package/src/components/ui/uptime.tsx +106 -0
  59. package/src/lib/config.ts +195 -0
  60. package/src/lib/gateway-connection.ts +391 -0
  61. package/src/lib/hooks/use-openclaw.ts +163 -0
  62. package/src/lib/utils.ts +6 -0
  63. package/tsconfig.json +34 -0
@@ -0,0 +1,171 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export interface ClockProps {
7
+ size?: number;
8
+ variant?: "solid" | "glass";
9
+ className?: string;
10
+ }
11
+
12
+ function Clock({ size = 200, variant = "solid", className }: ClockProps) {
13
+ const [mounted, setMounted] = useState(false);
14
+ const [time, setTime] = useState<Date | null>(null);
15
+
16
+ useEffect(() => {
17
+ setMounted(true);
18
+ setTime(new Date());
19
+ const timer = setInterval(() => {
20
+ setTime(new Date());
21
+ }, 1000);
22
+ return () => clearInterval(timer);
23
+ }, []);
24
+
25
+ const seconds = time?.getSeconds() ?? 0;
26
+ const minutes = time?.getMinutes() ?? 0;
27
+ const hours = (time?.getHours() ?? 0) % 12;
28
+
29
+ const secondDeg = seconds * 6;
30
+ const minuteDeg = minutes * 6 + seconds * 0.1;
31
+ const hourDeg = hours * 30 + minutes * 0.5;
32
+
33
+ const center = size / 2;
34
+ const clockRadius = size * 0.45;
35
+
36
+ const ticks = [];
37
+ for (let i = 0; i < 60; i++) {
38
+ const angle = (i * 6 - 90) * (Math.PI / 180);
39
+ const isHour = i % 5 === 0;
40
+ const innerRadius = isHour ? clockRadius * 0.85 : clockRadius * 0.92;
41
+ const outerRadius = clockRadius * 0.98;
42
+
43
+ const x1 = center + innerRadius * Math.cos(angle);
44
+ const y1 = center + innerRadius * Math.sin(angle);
45
+ const x2 = center + outerRadius * Math.cos(angle);
46
+ const y2 = center + outerRadius * Math.sin(angle);
47
+
48
+ ticks.push(
49
+ <line
50
+ key={i}
51
+ x1={x1}
52
+ y1={y1}
53
+ x2={x2}
54
+ y2={y2}
55
+ stroke="currentColor"
56
+ strokeWidth={isHour ? 1.5 : 0.5}
57
+ className="text-foreground-muted"
58
+ />
59
+ );
60
+ }
61
+
62
+ const numerals = [
63
+ { num: "12", angle: -90 },
64
+ { num: "3", angle: 0 },
65
+ { num: "6", angle: 90 },
66
+ { num: "9", angle: 180 },
67
+ ];
68
+
69
+ const numeralRadius = clockRadius * 0.7;
70
+
71
+ if (!mounted) {
72
+ return (
73
+ <div
74
+ className={cn(
75
+ "rounded-[20px] shadow-lg select-none",
76
+ variant === "glass" ? "glass" : "bg-surface border border-border",
77
+ className
78
+ )}
79
+ style={{ width: size, height: size }}
80
+ />
81
+ );
82
+ }
83
+
84
+ return (
85
+ <div
86
+ className={cn(
87
+ "rounded-[20px] shadow-lg select-none",
88
+ variant === "glass" ? "glass" : "bg-surface border border-border",
89
+ className
90
+ )}
91
+ style={{ width: size, height: size }}
92
+ >
93
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
94
+ {ticks}
95
+
96
+ {numerals.map(({ num, angle }) => {
97
+ const rad = angle * (Math.PI / 180);
98
+ const x = center + numeralRadius * Math.cos(rad);
99
+ const y = center + numeralRadius * Math.sin(rad);
100
+ return (
101
+ <text
102
+ key={num}
103
+ x={x}
104
+ y={y}
105
+ textAnchor="middle"
106
+ dominantBaseline="central"
107
+ className="fill-foreground/50 font-sans font-medium"
108
+ style={{ fontSize: size * 0.09 }}
109
+ >
110
+ {num}
111
+ </text>
112
+ );
113
+ })}
114
+
115
+ <line
116
+ x1={center}
117
+ y1={center - size * 0.06}
118
+ x2={center}
119
+ y2={center - clockRadius * 0.5}
120
+ stroke="currentColor"
121
+ strokeWidth={3}
122
+ strokeLinecap="round"
123
+ className="text-foreground"
124
+ style={{
125
+ transformOrigin: `${center}px ${center}px`,
126
+ transform: `rotate(${hourDeg}deg)`,
127
+ }}
128
+ />
129
+
130
+ <line
131
+ x1={center}
132
+ y1={center - size * 0.06}
133
+ x2={center}
134
+ y2={center - clockRadius * 0.75}
135
+ stroke="currentColor"
136
+ strokeWidth={2}
137
+ strokeLinecap="round"
138
+ className="text-foreground"
139
+ style={{
140
+ transformOrigin: `${center}px ${center}px`,
141
+ transform: `rotate(${minuteDeg}deg)`,
142
+ }}
143
+ />
144
+
145
+ <line
146
+ x1={center}
147
+ y1={center + clockRadius * 0.15}
148
+ x2={center}
149
+ y2={center - clockRadius * 0.8}
150
+ stroke="currentColor"
151
+ strokeWidth={1}
152
+ strokeLinecap="round"
153
+ className="text-error"
154
+ style={{
155
+ transformOrigin: `${center}px ${center}px`,
156
+ transform: `rotate(${secondDeg}deg)`,
157
+ }}
158
+ />
159
+
160
+ <circle
161
+ cx={center}
162
+ cy={center}
163
+ r={size * 0.025}
164
+ className="fill-error"
165
+ />
166
+ </svg>
167
+ </div>
168
+ );
169
+ }
170
+
171
+ export { Clock };
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type HTMLAttributes } from "react";
4
+ import { X } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export interface DialogProps extends HTMLAttributes<HTMLDivElement> {
8
+ open?: boolean;
9
+ onClose?: () => void;
10
+ }
11
+
12
+ const Dialog = forwardRef<HTMLDivElement, DialogProps>(
13
+ ({ className, open = false, onClose, children, ...props }, ref) => {
14
+ if (!open) return null;
15
+
16
+ return (
17
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
18
+ <div
19
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm"
20
+ onClick={onClose}
21
+ />
22
+ <div
23
+ className={cn(
24
+ "relative z-50 w-full max-w-lg rounded-[var(--radius-lg)] bg-surface border border-border p-6 shadow-xl",
25
+ className
26
+ )}
27
+ ref={ref}
28
+ {...props}
29
+ >
30
+ {onClose && (
31
+ <button
32
+ onClick={onClose}
33
+ className="absolute right-4 top-4 p-1 rounded-[var(--radius-sm)] text-foreground-muted hover:text-foreground hover:bg-surface-hover transition-colors"
34
+ >
35
+ <X className="h-4 w-4" />
36
+ </button>
37
+ )}
38
+ {children}
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+ );
44
+
45
+ Dialog.displayName = "Dialog";
46
+
47
+ const DialogHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
48
+ ({ className, ...props }, ref) => {
49
+ return (
50
+ <div
51
+ className={cn("mb-4", className)}
52
+ ref={ref}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+ );
58
+
59
+ DialogHeader.displayName = "DialogHeader";
60
+
61
+ const DialogTitle = forwardRef<
62
+ HTMLHeadingElement,
63
+ HTMLAttributes<HTMLHeadingElement>
64
+ >(({ className, ...props }, ref) => {
65
+ return (
66
+ <h2
67
+ className={cn("text-lg font-semibold text-foreground", className)}
68
+ ref={ref}
69
+ {...props}
70
+ />
71
+ );
72
+ });
73
+
74
+ DialogTitle.displayName = "DialogTitle";
75
+
76
+ const DialogDescription = forwardRef<
77
+ HTMLParagraphElement,
78
+ HTMLAttributes<HTMLParagraphElement>
79
+ >(({ className, ...props }, ref) => {
80
+ return (
81
+ <p
82
+ className={cn("text-sm text-foreground-secondary mt-1", className)}
83
+ ref={ref}
84
+ {...props}
85
+ />
86
+ );
87
+ });
88
+
89
+ DialogDescription.displayName = "DialogDescription";
90
+
91
+ const DialogFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
92
+ ({ className, ...props }, ref) => {
93
+ return (
94
+ <div
95
+ className={cn("flex justify-end gap-3 mt-6", className)}
96
+ ref={ref}
97
+ {...props}
98
+ />
99
+ );
100
+ }
101
+ );
102
+
103
+ DialogFooter.displayName = "DialogFooter";
104
+
105
+ export { Dialog, DialogHeader, DialogTitle, DialogDescription, DialogFooter };
@@ -0,0 +1,34 @@
1
+ export { Button, type ButtonProps } from "./button";
2
+ export {
3
+ Card,
4
+ CardHeader,
5
+ CardTitle,
6
+ CardDescription,
7
+ CardContent,
8
+ CardFooter,
9
+ type CardProps,
10
+ } from "./card";
11
+ export { Input, Textarea, type InputProps, type TextareaProps } from "./input";
12
+ export { Badge, type BadgeProps } from "./badge";
13
+ export { Avatar, AvatarImage, AvatarFallback, type AvatarProps } from "./avatar";
14
+ export { Select, type SelectProps } from "./select";
15
+ export {
16
+ Dialog,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ DialogDescription,
20
+ DialogFooter,
21
+ type DialogProps,
22
+ } from "./dialog";
23
+ export { Alert, type AlertProps } from "./alert";
24
+ export { Tooltip, type TooltipProps } from "./tooltip";
25
+ export { Toggle, type ToggleProps } from "./toggle";
26
+ export { Checkbox, type CheckboxProps } from "./checkbox";
27
+ export { RadioGroup, RadioGroupItem, type RadioGroupProps, type RadioGroupItemProps } from "./radio";
28
+ export { OptionCardGroup, OptionCard, CheckboxCard, type OptionCardGroupProps, type OptionCardProps, type CheckboxCardProps } from "./option-card";
29
+ export { ToggleGroup, ToggleGroupItem, type ToggleGroupProps, type ToggleGroupItemProps } from "./toggle-group";
30
+ export { Tabs, TabsList, TabsTrigger, TabsContent, type TabsProps, type TabsListProps, type TabsTriggerProps, type TabsContentProps } from "./tabs";
31
+ export { Progress, type ProgressProps } from "./progress";
32
+ export { Slider, type SliderProps } from "./slider";
33
+ export { Uptime, type UptimeProps, type UptimeStatus } from "./uptime";
34
+ export { Clock, type ClockProps } from "./clock";
@@ -0,0 +1,112 @@
1
+ import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
5
+ error?: boolean;
6
+ label?: string;
7
+ startAddon?: ReactNode;
8
+ endAddon?: ReactNode;
9
+ }
10
+
11
+ const Input = forwardRef<HTMLInputElement, InputProps>(
12
+ ({ className, type = "text", error, label, id, startAddon, endAddon, ...props }, ref) => {
13
+ const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
14
+ const hasAddon = startAddon || endAddon;
15
+
16
+ return (
17
+ <div className="w-full">
18
+ {label && (
19
+ <label htmlFor={inputId} className="form-label">
20
+ {label}
21
+ </label>
22
+ )}
23
+ {hasAddon ? (
24
+ <div
25
+ className={cn(
26
+ "flex items-center input-base",
27
+ error && "error",
28
+ "focus-within:border-[var(--input-focus)]"
29
+ )}
30
+ >
31
+ {startAddon && (
32
+ <span className="text-foreground-muted shrink-0 select-none">
33
+ {startAddon}
34
+ </span>
35
+ )}
36
+ <input
37
+ id={inputId}
38
+ type={type}
39
+ className={cn(
40
+ "flex-1 bg-transparent border-0 p-0 text-sm text-foreground placeholder:text-foreground-muted",
41
+ "focus:outline-none focus:ring-0",
42
+ startAddon && "pl-2",
43
+ endAddon && "pr-2",
44
+ className
45
+ )}
46
+ aria-invalid={error}
47
+ ref={ref}
48
+ {...props}
49
+ />
50
+ {endAddon && (
51
+ <span className="text-foreground-muted shrink-0 select-none">
52
+ {endAddon}
53
+ </span>
54
+ )}
55
+ </div>
56
+ ) : (
57
+ <input
58
+ id={inputId}
59
+ type={type}
60
+ className={cn(
61
+ "input-base flex placeholder:text-foreground-muted",
62
+ error && "error",
63
+ className
64
+ )}
65
+ aria-invalid={error}
66
+ ref={ref}
67
+ {...props}
68
+ />
69
+ )}
70
+ </div>
71
+ );
72
+ }
73
+ );
74
+
75
+ Input.displayName = "Input";
76
+
77
+ export interface TextareaProps
78
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
79
+ error?: boolean;
80
+ label?: string;
81
+ }
82
+
83
+ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
84
+ ({ className, error, label, id, ...props }, ref) => {
85
+ const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
86
+
87
+ return (
88
+ <div className="w-full">
89
+ {label && (
90
+ <label htmlFor={inputId} className="form-label">
91
+ {label}
92
+ </label>
93
+ )}
94
+ <textarea
95
+ id={inputId}
96
+ className={cn(
97
+ "input-base flex min-h-[100px] h-auto placeholder:text-foreground-muted resize-none",
98
+ error && "error",
99
+ className
100
+ )}
101
+ aria-invalid={error}
102
+ ref={ref}
103
+ {...props}
104
+ />
105
+ </div>
106
+ );
107
+ }
108
+ );
109
+
110
+ Textarea.displayName = "Textarea";
111
+
112
+ export { Input, Textarea };
@@ -0,0 +1,151 @@
1
+ "use client";
2
+
3
+ import { forwardRef, createContext, useContext, type HTMLAttributes, type ReactNode } from "react";
4
+ import { Check } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ interface OptionCardGroupContextValue {
8
+ value: string;
9
+ onValueChange: (value: string) => void;
10
+ }
11
+
12
+ const OptionCardGroupContext = createContext<OptionCardGroupContextValue | null>(null);
13
+
14
+ export interface OptionCardGroupProps extends HTMLAttributes<HTMLDivElement> {
15
+ value: string;
16
+ onValueChange: (value: string) => void;
17
+ }
18
+
19
+ const OptionCardGroup = forwardRef<HTMLDivElement, OptionCardGroupProps>(
20
+ ({ className, value, onValueChange, children, ...props }, ref) => {
21
+ return (
22
+ <OptionCardGroupContext.Provider value={{ value, onValueChange }}>
23
+ <div
24
+ ref={ref}
25
+ role="radiogroup"
26
+ className={cn("flex flex-col gap-3", className)}
27
+ {...props}
28
+ >
29
+ {children}
30
+ </div>
31
+ </OptionCardGroupContext.Provider>
32
+ );
33
+ }
34
+ );
35
+
36
+ OptionCardGroup.displayName = "OptionCardGroup";
37
+
38
+ export interface OptionCardProps extends HTMLAttributes<HTMLButtonElement> {
39
+ value: string;
40
+ disabled?: boolean;
41
+ children: ReactNode;
42
+ }
43
+
44
+ const OptionCard = forwardRef<HTMLButtonElement, OptionCardProps>(
45
+ ({ className, value, disabled = false, children, ...props }, ref) => {
46
+ const context = useContext(OptionCardGroupContext);
47
+
48
+ if (!context) {
49
+ throw new Error("OptionCard must be used within an OptionCardGroup");
50
+ }
51
+
52
+ const isSelected = context.value === value;
53
+
54
+ const handleClick = () => {
55
+ if (!disabled) {
56
+ context.onValueChange(value);
57
+ }
58
+ };
59
+
60
+ return (
61
+ <button
62
+ ref={ref}
63
+ type="button"
64
+ role="radio"
65
+ aria-checked={isSelected}
66
+ data-state={isSelected ? "checked" : "unchecked"}
67
+ disabled={disabled}
68
+ onClick={handleClick}
69
+ className={cn(
70
+ "flex items-center gap-4 w-full px-4 py-3 rounded-[var(--radius-md)] border-2 transition-all text-left",
71
+ "interactive",
72
+ isSelected
73
+ ? "border-accent bg-accent/5"
74
+ : "border-[var(--input-border)] bg-[var(--input-background)] hover:border-border-hover",
75
+ disabled && "opacity-50",
76
+ className
77
+ )}
78
+ {...props}
79
+ >
80
+ <div
81
+ className={cn(
82
+ "h-5 w-5 shrink-0 rounded-full border-2 flex items-center justify-center transition-colors",
83
+ isSelected ? "border-accent" : "border-[var(--input-border)]"
84
+ )}
85
+ >
86
+ {isSelected && (
87
+ <span className="h-2.5 w-2.5 rounded-full bg-accent" />
88
+ )}
89
+ </div>
90
+ <div className="flex-1">{children}</div>
91
+ </button>
92
+ );
93
+ }
94
+ );
95
+
96
+ OptionCard.displayName = "OptionCard";
97
+
98
+ export interface CheckboxCardProps extends HTMLAttributes<HTMLButtonElement> {
99
+ checked?: boolean;
100
+ onCheckedChange?: (checked: boolean) => void;
101
+ disabled?: boolean;
102
+ children: ReactNode;
103
+ }
104
+
105
+ const CheckboxCard = forwardRef<HTMLButtonElement, CheckboxCardProps>(
106
+ ({ className, checked = false, onCheckedChange, disabled = false, children, ...props }, ref) => {
107
+ const handleClick = () => {
108
+ if (!disabled) {
109
+ onCheckedChange?.(!checked);
110
+ }
111
+ };
112
+
113
+ return (
114
+ <button
115
+ ref={ref}
116
+ type="button"
117
+ role="checkbox"
118
+ aria-checked={checked}
119
+ data-state={checked ? "checked" : "unchecked"}
120
+ disabled={disabled}
121
+ onClick={handleClick}
122
+ className={cn(
123
+ "flex items-center gap-4 w-full px-4 py-3 rounded-[var(--radius-md)] border-2 transition-all text-left",
124
+ "interactive",
125
+ checked
126
+ ? "border-accent bg-accent/5"
127
+ : "border-[var(--input-border)] bg-[var(--input-background)] hover:border-border-hover",
128
+ disabled && "opacity-50",
129
+ className
130
+ )}
131
+ {...props}
132
+ >
133
+ <div
134
+ className={cn(
135
+ "h-5 w-5 shrink-0 rounded-[var(--radius-sm)] border-2 flex items-center justify-center transition-colors",
136
+ checked
137
+ ? "border-accent bg-accent text-white"
138
+ : "border-[var(--input-border)] bg-[var(--input-background)]"
139
+ )}
140
+ >
141
+ {checked && <Check className="h-3.5 w-3.5" strokeWidth={3} />}
142
+ </div>
143
+ <div className="flex-1">{children}</div>
144
+ </button>
145
+ );
146
+ }
147
+ );
148
+
149
+ CheckboxCard.displayName = "CheckboxCard";
150
+
151
+ export { OptionCardGroup, OptionCard, CheckboxCard };
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { TrendingUp, TrendingDown } from "lucide-react";
5
+
6
+ export interface ProgressProps {
7
+ value: number;
8
+ max: number;
9
+ trend?: number;
10
+ trendLabel?: string;
11
+ variant?: "success" | "accent" | "warning" | "error";
12
+ size?: "sm" | "md" | "lg";
13
+ className?: string;
14
+ }
15
+
16
+ function Progress({
17
+ value,
18
+ max,
19
+ trend,
20
+ trendLabel,
21
+ variant = "success",
22
+ size = "md",
23
+ className,
24
+ }: ProgressProps) {
25
+ const percentage = Math.round((value / max) * 100);
26
+
27
+ const barColors = {
28
+ success: "bg-success",
29
+ accent: "bg-accent",
30
+ warning: "bg-warning",
31
+ error: "bg-error",
32
+ };
33
+
34
+ const trendColors = {
35
+ success: "text-success",
36
+ accent: "text-accent",
37
+ warning: "text-warning",
38
+ error: "text-error",
39
+ };
40
+
41
+ const sizes = {
42
+ sm: {
43
+ percentage: "text-xl",
44
+ fraction: "text-xs",
45
+ bar: "h-1.5",
46
+ trend: "text-xs",
47
+ },
48
+ md: {
49
+ percentage: "text-2xl",
50
+ fraction: "text-sm",
51
+ bar: "h-2",
52
+ trend: "text-sm",
53
+ },
54
+ lg: {
55
+ percentage: "text-4xl",
56
+ fraction: "text-base",
57
+ bar: "h-3",
58
+ trend: "text-base",
59
+ },
60
+ };
61
+
62
+ const sizeConfig = sizes[size];
63
+
64
+ return (
65
+ <div className={cn("space-y-2", className)}>
66
+ <div className="flex items-baseline justify-between">
67
+ <span className={cn("font-bold text-foreground", sizeConfig.percentage)}>
68
+ {percentage}%
69
+ </span>
70
+ <span className={cn("text-foreground-secondary", sizeConfig.fraction)}>
71
+ {value.toLocaleString()} of {max.toLocaleString()}
72
+ </span>
73
+ </div>
74
+
75
+ <div className={cn("w-full bg-border rounded-full overflow-hidden", sizeConfig.bar)}>
76
+ <div
77
+ className={cn("h-full rounded-full transition-all duration-500", barColors[variant])}
78
+ style={{ width: `${percentage}%` }}
79
+ />
80
+ </div>
81
+
82
+ {trend !== undefined && (
83
+ <div className="flex items-center gap-2">
84
+ {trend >= 0 ? (
85
+ <TrendingUp className={cn("h-4 w-4", trendColors[variant])} />
86
+ ) : (
87
+ <TrendingDown className="h-4 w-4 text-error" />
88
+ )}
89
+ <span className={cn("font-medium", trend >= 0 ? trendColors[variant] : "text-error", sizeConfig.trend)}>
90
+ {trend >= 0 ? "+" : ""}{trend}%
91
+ </span>
92
+ {trendLabel && (
93
+ <span className={cn("text-foreground-secondary", sizeConfig.trend)}>
94
+ {trendLabel}
95
+ </span>
96
+ )}
97
+ </div>
98
+ )}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ export { Progress };