@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,174 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface GoalWidgetProps {
|
|
6
|
+
title: string;
|
|
7
|
+
value: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
unit?: string;
|
|
10
|
+
status?: string;
|
|
11
|
+
statusColor?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
highlight?: string;
|
|
14
|
+
icon?: React.ReactNode;
|
|
15
|
+
size?: "sm" | "md" | "lg";
|
|
16
|
+
variant?: "solid" | "glass";
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sizeConfig = {
|
|
21
|
+
sm: {
|
|
22
|
+
containerSize: 140,
|
|
23
|
+
padding: "p-2",
|
|
24
|
+
gaugeSize: 86,
|
|
25
|
+
strokeWidth: 5,
|
|
26
|
+
titleSize: "text-xs",
|
|
27
|
+
valueSize: "text-base",
|
|
28
|
+
statusSize: "text-[10px]",
|
|
29
|
+
descSize: "text-[10px]",
|
|
30
|
+
iconContainer: "w-5 h-5",
|
|
31
|
+
iconSize: "w-3 h-3",
|
|
32
|
+
},
|
|
33
|
+
md: {
|
|
34
|
+
containerSize: 180,
|
|
35
|
+
padding: "p-3",
|
|
36
|
+
gaugeSize: 100,
|
|
37
|
+
strokeWidth: 6,
|
|
38
|
+
titleSize: "text-xs",
|
|
39
|
+
valueSize: "text-xl",
|
|
40
|
+
statusSize: "text-xs",
|
|
41
|
+
descSize: "text-[10px]",
|
|
42
|
+
iconContainer: "w-5 h-5",
|
|
43
|
+
iconSize: "w-3 h-3",
|
|
44
|
+
},
|
|
45
|
+
lg: {
|
|
46
|
+
containerSize: 220,
|
|
47
|
+
padding: "p-3",
|
|
48
|
+
gaugeSize: 130,
|
|
49
|
+
strokeWidth: 7,
|
|
50
|
+
titleSize: "text-sm",
|
|
51
|
+
valueSize: "text-2xl",
|
|
52
|
+
statusSize: "text-xs",
|
|
53
|
+
descSize: "text-xs",
|
|
54
|
+
iconContainer: "w-6 h-6",
|
|
55
|
+
iconSize: "w-3.5 h-3.5",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function GoalWidget({
|
|
60
|
+
title,
|
|
61
|
+
value,
|
|
62
|
+
max = 100,
|
|
63
|
+
unit,
|
|
64
|
+
status,
|
|
65
|
+
statusColor = "#f97316",
|
|
66
|
+
description,
|
|
67
|
+
highlight,
|
|
68
|
+
icon,
|
|
69
|
+
size = "md",
|
|
70
|
+
variant = "solid",
|
|
71
|
+
className,
|
|
72
|
+
}: GoalWidgetProps) {
|
|
73
|
+
const config = sizeConfig[size];
|
|
74
|
+
const percentage = Math.min((value / max) * 100, 100);
|
|
75
|
+
|
|
76
|
+
const gaugeSize = config.gaugeSize;
|
|
77
|
+
const strokeWidth = config.strokeWidth;
|
|
78
|
+
const radius = (gaugeSize - strokeWidth) / 2;
|
|
79
|
+
const circumference = radius * Math.PI * 1.5;
|
|
80
|
+
const offset = circumference - (percentage / 100) * circumference;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
className={cn(
|
|
85
|
+
config.padding,
|
|
86
|
+
"space-y-1 flex flex-col rounded-[var(--radius-lg)]",
|
|
87
|
+
variant === "glass" ? "glass" : "bg-surface border border-border",
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
style={{ width: config.containerSize, height: config.containerSize }}
|
|
91
|
+
>
|
|
92
|
+
<div className="flex items-start justify-between">
|
|
93
|
+
<div className={cn(config.titleSize, "font-semibold text-foreground")}>{title}</div>
|
|
94
|
+
{icon && (
|
|
95
|
+
<div
|
|
96
|
+
className={cn(config.iconContainer, "rounded-full flex items-center justify-center")}
|
|
97
|
+
style={{ backgroundColor: statusColor }}
|
|
98
|
+
>
|
|
99
|
+
<div className={config.iconSize}>
|
|
100
|
+
{icon}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div className="flex-1 flex items-center justify-center">
|
|
107
|
+
<div className="relative" style={{ width: gaugeSize, height: gaugeSize }}>
|
|
108
|
+
<svg
|
|
109
|
+
width={gaugeSize}
|
|
110
|
+
height={gaugeSize}
|
|
111
|
+
className="transform -rotate-[225deg]"
|
|
112
|
+
style={{ overflow: 'visible' }}
|
|
113
|
+
>
|
|
114
|
+
<circle
|
|
115
|
+
cx={gaugeSize / 2}
|
|
116
|
+
cy={gaugeSize / 2}
|
|
117
|
+
r={radius}
|
|
118
|
+
fill="none"
|
|
119
|
+
stroke="currentColor"
|
|
120
|
+
strokeWidth={strokeWidth}
|
|
121
|
+
strokeLinecap="round"
|
|
122
|
+
strokeDasharray={circumference}
|
|
123
|
+
strokeDashoffset={0}
|
|
124
|
+
className="text-foreground/10"
|
|
125
|
+
/>
|
|
126
|
+
<circle
|
|
127
|
+
cx={gaugeSize / 2}
|
|
128
|
+
cy={gaugeSize / 2}
|
|
129
|
+
r={radius}
|
|
130
|
+
fill="none"
|
|
131
|
+
stroke={statusColor}
|
|
132
|
+
strokeWidth={strokeWidth}
|
|
133
|
+
strokeLinecap="round"
|
|
134
|
+
strokeDasharray={circumference}
|
|
135
|
+
strokeDashoffset={offset}
|
|
136
|
+
/>
|
|
137
|
+
</svg>
|
|
138
|
+
|
|
139
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
140
|
+
<span className={cn(config.valueSize, "font-bold text-foreground")}>
|
|
141
|
+
{value}{unit}
|
|
142
|
+
</span>
|
|
143
|
+
{status && (
|
|
144
|
+
<span
|
|
145
|
+
className={cn(config.statusSize, "font-medium")}
|
|
146
|
+
style={{ color: statusColor }}
|
|
147
|
+
>
|
|
148
|
+
{status}
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{description && (
|
|
156
|
+
<p className={cn(config.descSize, "text-foreground/60 text-center")}>
|
|
157
|
+
{highlight ? (
|
|
158
|
+
<>
|
|
159
|
+
{description.split(highlight)[0]}
|
|
160
|
+
<span style={{ color: statusColor }} className="font-medium">
|
|
161
|
+
{highlight}
|
|
162
|
+
</span>
|
|
163
|
+
{description.split(highlight)[1]}
|
|
164
|
+
</>
|
|
165
|
+
) : (
|
|
166
|
+
description
|
|
167
|
+
)}
|
|
168
|
+
</p>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { GoalWidget };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { useTheme } from "next-themes";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
|
|
7
|
+
function getGreeting(): string {
|
|
8
|
+
const hour = new Date().getHours();
|
|
9
|
+
if (hour < 12) return "Good morning";
|
|
10
|
+
if (hour < 17) return "Good afternoon";
|
|
11
|
+
return "Good evening";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatDate(): string {
|
|
15
|
+
return new Date().toLocaleDateString("en-US", {
|
|
16
|
+
weekday: "long",
|
|
17
|
+
month: "long",
|
|
18
|
+
day: "numeric",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GreetingWidgetProps {
|
|
23
|
+
name?: string;
|
|
24
|
+
variant?: "solid" | "glass";
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function GreetingWidget({ name = "Brian", variant = "solid", className }: GreetingWidgetProps) {
|
|
29
|
+
const { theme } = useTheme();
|
|
30
|
+
const [mounted, setMounted] = useState(false);
|
|
31
|
+
const [greeting, setGreeting] = useState("");
|
|
32
|
+
const [date, setDate] = useState("");
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setMounted(true);
|
|
36
|
+
setGreeting(getGreeting());
|
|
37
|
+
setDate(formatDate());
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const isDark = mounted && theme === "dark";
|
|
41
|
+
const textColor = variant === "solid" ? undefined : (isDark ? 'white' : 'black');
|
|
42
|
+
|
|
43
|
+
if (!mounted) {
|
|
44
|
+
return (
|
|
45
|
+
<div className={cn(
|
|
46
|
+
"py-6 flex flex-col justify-center h-full",
|
|
47
|
+
variant === "solid" && "bg-surface rounded-[var(--radius-lg)] px-6 border border-border",
|
|
48
|
+
className
|
|
49
|
+
)}>
|
|
50
|
+
<p className={cn("text-sm mb-1", variant === "solid" && "text-foreground-secondary")}> </p>
|
|
51
|
+
<h1 className={cn("text-4xl font-semibold", variant === "solid" && "text-foreground")}> </h1>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className={cn(
|
|
58
|
+
"py-6 flex flex-col justify-center h-full",
|
|
59
|
+
variant === "solid" && "bg-surface rounded-[var(--radius-lg)] px-6 border border-border",
|
|
60
|
+
className
|
|
61
|
+
)}>
|
|
62
|
+
<p
|
|
63
|
+
className={cn("text-sm mb-1", variant === "solid" && "text-foreground-secondary")}
|
|
64
|
+
style={textColor ? { color: textColor } : undefined}
|
|
65
|
+
>
|
|
66
|
+
{date}
|
|
67
|
+
</p>
|
|
68
|
+
<h1
|
|
69
|
+
className={cn("text-4xl font-semibold", variant === "solid" && "text-foreground")}
|
|
70
|
+
style={textColor ? { color: textColor } : undefined}
|
|
71
|
+
>
|
|
72
|
+
{greeting}, {name}
|
|
73
|
+
</h1>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { GreetingWidget };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { GlassCard, type GlassCardProps } from "./glass-card";
|
|
2
|
+
export { GreetingWidget, type GreetingWidgetProps } from "./greeting-widget";
|
|
3
|
+
export { WeatherWidget, type WeatherWidgetProps } from "./weather-widget";
|
|
4
|
+
export { AgentStatusWidget, type AgentStatusWidgetProps } from "./agent-status";
|
|
5
|
+
export { StatWidget, type StatWidgetProps } from "./stat-widget";
|
|
6
|
+
export { StockWidget, type StockWidgetProps } from "./stock-widget";
|
|
7
|
+
export { GoalWidget, type GoalWidgetProps } from "./goal-widget";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type LucideIcon } from "lucide-react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
import { GlassCard } from "./glass-card";
|
|
4
|
+
import { Card } from "@/components/ui/card";
|
|
5
|
+
|
|
6
|
+
export interface StatWidgetProps {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string | number;
|
|
9
|
+
change?: string;
|
|
10
|
+
changeType?: "positive" | "negative" | "neutral";
|
|
11
|
+
icon?: LucideIcon;
|
|
12
|
+
className?: string;
|
|
13
|
+
variant?: "solid" | "glass";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function StatWidget({
|
|
17
|
+
label,
|
|
18
|
+
value,
|
|
19
|
+
change,
|
|
20
|
+
changeType = "neutral",
|
|
21
|
+
icon: Icon,
|
|
22
|
+
className,
|
|
23
|
+
variant = "solid",
|
|
24
|
+
}: StatWidgetProps) {
|
|
25
|
+
const content = (
|
|
26
|
+
<div className="flex items-start justify-between">
|
|
27
|
+
<div>
|
|
28
|
+
<p className="text-sm text-foreground-muted mb-1">{label}</p>
|
|
29
|
+
<p className="text-3xl font-semibold text-foreground">{value}</p>
|
|
30
|
+
{change && (
|
|
31
|
+
<p
|
|
32
|
+
className={cn("text-xs mt-2", {
|
|
33
|
+
"text-success": changeType === "positive",
|
|
34
|
+
"text-error": changeType === "negative",
|
|
35
|
+
"text-foreground-muted": changeType === "neutral",
|
|
36
|
+
})}
|
|
37
|
+
>
|
|
38
|
+
{change}
|
|
39
|
+
</p>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
{Icon && (
|
|
43
|
+
<div className="p-2 rounded-[var(--radius-md)] bg-accent/10">
|
|
44
|
+
<Icon className="h-5 w-5 text-accent" />
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (variant === "solid") {
|
|
51
|
+
return (
|
|
52
|
+
<Card variant="bordered" className={cn("p-6", className)}>
|
|
53
|
+
{content}
|
|
54
|
+
</Card>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return <GlassCard className={className}>{content}</GlassCard>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { StatWidget };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { TrendingUp, TrendingDown } from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
ResponsiveContainer,
|
|
7
|
+
AreaChart,
|
|
8
|
+
Area,
|
|
9
|
+
ReferenceLine,
|
|
10
|
+
YAxis,
|
|
11
|
+
} from "recharts";
|
|
12
|
+
|
|
13
|
+
export interface StockWidgetProps {
|
|
14
|
+
ticker: string;
|
|
15
|
+
companyName: string;
|
|
16
|
+
price: number;
|
|
17
|
+
change: number;
|
|
18
|
+
changePercent: number;
|
|
19
|
+
currency?: string;
|
|
20
|
+
updatedAt?: string;
|
|
21
|
+
logo?: React.ReactNode;
|
|
22
|
+
chartData?: number[];
|
|
23
|
+
variant?: "solid" | "glass";
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function BitcoinLogo() {
|
|
28
|
+
return (
|
|
29
|
+
<svg viewBox="0 0 32 32" className="w-full h-full block" aria-hidden="true">
|
|
30
|
+
<circle cx="16" cy="16" r="16" fill="#F7931A" />
|
|
31
|
+
<path
|
|
32
|
+
fill="white"
|
|
33
|
+
d="M22.5 14.1c.3-2-1.2-3.1-3.3-3.8l.7-2.7-1.7-.4-.7 2.6c-.4-.1-.9-.2-1.4-.3l.7-2.7-1.7-.4-.7 2.7c-.4-.1-.7-.2-1-.2l-2.3-.6-.4 1.8s1.2.3 1.2.3c.7.2.8.6.8 1l-.8 3.2c0 0 .1 0 .2.1h-.2l-1.1 4.5c-.1.2-.3.5-.8.4 0 0-1.2-.3-1.2-.3l-.8 1.9 2.2.5c.4.1.8.2 1.2.3l-.7 2.8 1.7.4.7-2.7c.5.1.9.2 1.4.3l-.7 2.7 1.7.4.7-2.8c2.8.5 4.9.3 5.8-2.2.7-2-.1-3.2-1.5-3.9 1.1-.3 1.9-1 2.1-2.5zm-3.8 5.3c-.5 2-3.9.9-5 .7l.9-3.6c1.1.3 4.6.8 4.1 2.9zm.5-5.4c-.5 1.8-3.3.9-4.2.7l.8-3.2c.9.2 3.9.6 3.4 2.5z"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function StockWidget({
|
|
40
|
+
ticker,
|
|
41
|
+
companyName,
|
|
42
|
+
price,
|
|
43
|
+
change,
|
|
44
|
+
changePercent,
|
|
45
|
+
currency = "£",
|
|
46
|
+
updatedAt = "2m ago",
|
|
47
|
+
logo,
|
|
48
|
+
chartData = [],
|
|
49
|
+
variant = "solid",
|
|
50
|
+
className,
|
|
51
|
+
}: StockWidgetProps) {
|
|
52
|
+
const isPositive = change >= 0;
|
|
53
|
+
const trendColor = isPositive ? "#22c55e" : "#ef4444";
|
|
54
|
+
const normalizedTicker = ticker.trim().toUpperCase();
|
|
55
|
+
const effectiveLogo =
|
|
56
|
+
logo ?? (normalizedTicker === "BTC" || normalizedTicker === "XBT" ? <BitcoinLogo /> : null);
|
|
57
|
+
|
|
58
|
+
const formattedPrice = price.toLocaleString("de-DE", {
|
|
59
|
+
minimumFractionDigits: 2,
|
|
60
|
+
maximumFractionDigits: 2,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const formattedChange = Math.abs(change).toLocaleString("en-GB", {
|
|
64
|
+
minimumFractionDigits: 2,
|
|
65
|
+
maximumFractionDigits: 2,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const formattedPercent = Math.abs(changePercent).toFixed(2);
|
|
69
|
+
|
|
70
|
+
const data = chartData.map((value, index) => ({
|
|
71
|
+
index,
|
|
72
|
+
value,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const referenceValue = chartData.length > 0
|
|
76
|
+
? chartData.reduce((sum, val) => sum + val, 0) / chartData.length
|
|
77
|
+
: price;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={cn(
|
|
81
|
+
"p-3 space-y-2 rounded-[var(--radius-lg)]",
|
|
82
|
+
variant === "glass" ? "glass" : "bg-surface border border-border",
|
|
83
|
+
className
|
|
84
|
+
)}>
|
|
85
|
+
<div className="flex items-start justify-between">
|
|
86
|
+
<div className="flex items-center gap-2">
|
|
87
|
+
{effectiveLogo && (
|
|
88
|
+
<div className="w-6 h-6 flex items-center justify-center">
|
|
89
|
+
{effectiveLogo}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
<div>
|
|
93
|
+
<div className="text-xs font-semibold text-foreground">{ticker}</div>
|
|
94
|
+
<div className="text-xs text-foreground/60">{companyName}</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="text-right">
|
|
98
|
+
<div className="text-[10px] text-foreground/40">updated</div>
|
|
99
|
+
<div className="text-xs text-foreground/60">{updatedAt}</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="flex items-baseline gap-2">
|
|
104
|
+
<span className="text-2xl font-bold text-foreground tracking-tight">
|
|
105
|
+
{currency}{formattedPrice}
|
|
106
|
+
</span>
|
|
107
|
+
<div className="flex items-center gap-2">
|
|
108
|
+
<span
|
|
109
|
+
className="text-xs font-medium"
|
|
110
|
+
style={{ color: trendColor }}
|
|
111
|
+
>
|
|
112
|
+
{isPositive ? "+" : "-"}{currency}{formattedChange}
|
|
113
|
+
</span>
|
|
114
|
+
<span
|
|
115
|
+
className="text-[10px] font-semibold px-1.5 py-0.5 rounded flex items-center gap-0.5"
|
|
116
|
+
style={{ backgroundColor: trendColor, color: "white" }}
|
|
117
|
+
>
|
|
118
|
+
{isPositive ? (
|
|
119
|
+
<TrendingUp className="w-2.5 h-2.5" />
|
|
120
|
+
) : (
|
|
121
|
+
<TrendingDown className="w-2.5 h-2.5" />
|
|
122
|
+
)}
|
|
123
|
+
{formattedPercent}%
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{chartData.length >= 2 && (
|
|
129
|
+
<div className="h-10 -mx-1">
|
|
130
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
131
|
+
<AreaChart
|
|
132
|
+
data={data}
|
|
133
|
+
margin={{ top: 2, right: 0, left: 0, bottom: 0 }}
|
|
134
|
+
>
|
|
135
|
+
<defs>
|
|
136
|
+
<linearGradient id={`gradient-${ticker}`} x1="0" y1="0" x2="0" y2="1">
|
|
137
|
+
<stop offset="0%" stopColor={trendColor} stopOpacity={0.3} />
|
|
138
|
+
<stop offset="100%" stopColor={trendColor} stopOpacity={0} />
|
|
139
|
+
</linearGradient>
|
|
140
|
+
</defs>
|
|
141
|
+
<YAxis domain={["dataMin", "dataMax"]} hide />
|
|
142
|
+
<ReferenceLine
|
|
143
|
+
y={referenceValue}
|
|
144
|
+
stroke="rgba(255,255,255,0.2)"
|
|
145
|
+
strokeDasharray="3 3"
|
|
146
|
+
/>
|
|
147
|
+
<Area
|
|
148
|
+
type="linear"
|
|
149
|
+
dataKey="value"
|
|
150
|
+
stroke={trendColor}
|
|
151
|
+
strokeWidth={1.5}
|
|
152
|
+
fill={`url(#gradient-${ticker})`}
|
|
153
|
+
dot={false}
|
|
154
|
+
isAnimationActive={false}
|
|
155
|
+
/>
|
|
156
|
+
</AreaChart>
|
|
157
|
+
</ResponsiveContainer>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { StockWidget };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Cloud, Sun, CloudRain, CloudSnow, Wind } from "lucide-react";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
|
|
6
|
+
type WeatherCondition = "sunny" | "cloudy" | "rainy" | "snowy" | "windy";
|
|
7
|
+
|
|
8
|
+
export interface WeatherWidgetProps {
|
|
9
|
+
temperature?: number;
|
|
10
|
+
condition?: WeatherCondition;
|
|
11
|
+
location?: string;
|
|
12
|
+
high?: number;
|
|
13
|
+
low?: number;
|
|
14
|
+
variant?: "solid" | "glass";
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const weatherIcons: Record<WeatherCondition, typeof Sun> = {
|
|
19
|
+
sunny: Sun,
|
|
20
|
+
cloudy: Cloud,
|
|
21
|
+
rainy: CloudRain,
|
|
22
|
+
snowy: CloudSnow,
|
|
23
|
+
windy: Wind,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function WeatherWidget({
|
|
27
|
+
temperature = 12,
|
|
28
|
+
condition = "cloudy",
|
|
29
|
+
location = "Zurich",
|
|
30
|
+
high = 15,
|
|
31
|
+
low = 8,
|
|
32
|
+
variant = "solid",
|
|
33
|
+
className,
|
|
34
|
+
}: WeatherWidgetProps) {
|
|
35
|
+
const Icon = weatherIcons[condition];
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className={cn(
|
|
39
|
+
"rounded-[var(--radius-lg)] p-6",
|
|
40
|
+
variant === "glass" ? "glass" : "bg-surface border border-border",
|
|
41
|
+
className
|
|
42
|
+
)}>
|
|
43
|
+
<div className="flex items-center justify-between">
|
|
44
|
+
<div>
|
|
45
|
+
<p className="text-sm text-foreground-secondary mb-1">{location}</p>
|
|
46
|
+
<div className="flex items-baseline gap-1">
|
|
47
|
+
<span className="text-5xl font-light text-foreground">
|
|
48
|
+
{temperature}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="text-2xl text-foreground-secondary">°C</span>
|
|
51
|
+
</div>
|
|
52
|
+
<p className="text-sm text-foreground-secondary mt-2 capitalize">
|
|
53
|
+
{condition}
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex flex-col items-center gap-2">
|
|
57
|
+
<Icon className="h-12 w-12 text-foreground-secondary" strokeWidth={1.5} />
|
|
58
|
+
<div className="flex gap-3 text-xs text-foreground-muted">
|
|
59
|
+
<span>H: {high}°</span>
|
|
60
|
+
<span>L: {low}°</span>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { WeatherWidget };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils";
|
|
2
|
+
|
|
3
|
+
interface CastleIconProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function CastleIcon({ className }: CastleIconProps) {
|
|
8
|
+
return (
|
|
9
|
+
<svg
|
|
10
|
+
viewBox="0 0 512 512"
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
className={cn("fill-current", className)}
|
|
13
|
+
>
|
|
14
|
+
<g>
|
|
15
|
+
<path d="m124.809 408.11h262.382v103.89h-262.382z" />
|
|
16
|
+
<path d="m273.162 123.55h134.646l-34.459-44.772 33.515-46.459h-133.702v-32.319h-29.985v196.446h29.985z" />
|
|
17
|
+
<path d="m347.45 281.985h-50.335v-56.54h-79.107v56.54h-53.643v-56.54h-91.962v111.141c0 23.448 19.075 42.523 42.523 42.523h282.147c23.448 0 42.524-19.075 42.524-42.523v-111.14h-92.147z" />
|
|
18
|
+
</g>
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { KanbanBoard, type KanbanBoardProps, type ColumnData } from "./kanban-board";
|
|
2
|
+
export { KanbanColumn, type KanbanColumnProps } from "./kanban-column";
|
|
3
|
+
export { KanbanCard, SortableCard, type KanbanCardProps, type TaskPriority, type TaskStatus } from "./kanban-card";
|