@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.
- package/README.md +38 -1
- package/bin/castle.js +94 -0
- package/install.sh +722 -0
- package/next.config.ts +7 -0
- package/package.json +54 -5
- package/postcss.config.mjs +7 -0
- package/src/app/api/avatars/[id]/route.ts +75 -0
- package/src/app/api/openclaw/agents/route.ts +107 -0
- package/src/app/api/openclaw/config/route.ts +94 -0
- package/src/app/api/openclaw/events/route.ts +96 -0
- package/src/app/api/openclaw/logs/route.ts +59 -0
- package/src/app/api/openclaw/ping/route.ts +68 -0
- package/src/app/api/openclaw/restart/route.ts +65 -0
- package/src/app/api/openclaw/sessions/route.ts +62 -0
- package/src/app/globals.css +286 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +42 -0
- package/src/app/page.tsx +269 -0
- package/src/app/ui-kit/page.tsx +684 -0
- package/src/cli/onboarding.ts +576 -0
- package/src/components/dashboard/agent-status.tsx +107 -0
- package/src/components/dashboard/glass-card.tsx +28 -0
- package/src/components/dashboard/goal-widget.tsx +174 -0
- package/src/components/dashboard/greeting-widget.tsx +78 -0
- package/src/components/dashboard/index.ts +7 -0
- package/src/components/dashboard/stat-widget.tsx +61 -0
- package/src/components/dashboard/stock-widget.tsx +164 -0
- package/src/components/dashboard/weather-widget.tsx +68 -0
- package/src/components/icons/castle-icon.tsx +21 -0
- package/src/components/kanban/index.ts +3 -0
- package/src/components/kanban/kanban-board.tsx +391 -0
- package/src/components/kanban/kanban-card.tsx +137 -0
- package/src/components/kanban/kanban-column.tsx +98 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/layout/page-header.tsx +20 -0
- package/src/components/layout/sidebar.tsx +128 -0
- package/src/components/layout/theme-toggle.tsx +59 -0
- package/src/components/layout/user-menu.tsx +72 -0
- package/src/components/ui/alert.tsx +72 -0
- package/src/components/ui/avatar.tsx +87 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/button.tsx +43 -0
- package/src/components/ui/card.tsx +107 -0
- package/src/components/ui/checkbox.tsx +56 -0
- package/src/components/ui/clock.tsx +171 -0
- package/src/components/ui/dialog.tsx +105 -0
- package/src/components/ui/index.ts +34 -0
- package/src/components/ui/input.tsx +112 -0
- package/src/components/ui/option-card.tsx +151 -0
- package/src/components/ui/progress.tsx +103 -0
- package/src/components/ui/radio.tsx +109 -0
- package/src/components/ui/select.tsx +46 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/tabs.tsx +132 -0
- package/src/components/ui/toggle-group.tsx +85 -0
- package/src/components/ui/toggle.tsx +78 -0
- package/src/components/ui/tooltip.tsx +145 -0
- package/src/components/ui/uptime.tsx +106 -0
- package/src/lib/config.ts +195 -0
- package/src/lib/gateway-connection.ts +391 -0
- package/src/lib/hooks/use-openclaw.ts +163 -0
- package/src/lib/utils.ts +6 -0
- 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 };
|