@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,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
LayoutDashboard,
|
|
5
|
+
type LucideIcon,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import { Fragment } from "react";
|
|
8
|
+
import { CastleIcon } from "@/components/icons/castle-icon";
|
|
9
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
import Link from "next/link";
|
|
12
|
+
import { usePathname } from "next/navigation";
|
|
13
|
+
|
|
14
|
+
interface NavItem {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
icon: LucideIcon;
|
|
18
|
+
href: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SidebarProps {
|
|
22
|
+
activeItem?: string;
|
|
23
|
+
onNavigate?: (id: string) => void;
|
|
24
|
+
className?: string;
|
|
25
|
+
variant?: "glass" | "solid";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const navItems: NavItem[] = [
|
|
29
|
+
{
|
|
30
|
+
id: "dashboard",
|
|
31
|
+
label: "Dashboard",
|
|
32
|
+
icon: LayoutDashboard,
|
|
33
|
+
href: "/",
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function Sidebar({
|
|
38
|
+
activeItem = "dashboard",
|
|
39
|
+
onNavigate,
|
|
40
|
+
className,
|
|
41
|
+
variant = "solid"
|
|
42
|
+
}: SidebarProps) {
|
|
43
|
+
const pathname = usePathname();
|
|
44
|
+
const useLinks = !onNavigate;
|
|
45
|
+
|
|
46
|
+
const activeFromPath = (() => {
|
|
47
|
+
if (!pathname) return "dashboard";
|
|
48
|
+
if (pathname === "/") return "dashboard";
|
|
49
|
+
return "dashboard";
|
|
50
|
+
})();
|
|
51
|
+
|
|
52
|
+
const effectiveActive = useLinks ? activeFromPath : activeItem;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<aside
|
|
56
|
+
className={cn(
|
|
57
|
+
"fixed top-5 left-6 bottom-5 flex flex-col z-40 shadow-xl shadow-black/20 rounded-[28px] w-14",
|
|
58
|
+
variant === "glass" ? "glass" : "bg-surface border border-border",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{/* Header */}
|
|
63
|
+
<div className="flex items-center justify-center pt-5 pb-[60px]">
|
|
64
|
+
{useLinks ? (
|
|
65
|
+
<Link
|
|
66
|
+
href="/"
|
|
67
|
+
aria-label="Go to Dashboard"
|
|
68
|
+
className="flex items-center justify-center transition-opacity hover:opacity-85"
|
|
69
|
+
>
|
|
70
|
+
<CastleIcon className="h-[36px] w-[36px] min-h-[36px] min-w-[36px] shrink-0 text-[var(--logo-color)] -mt-[3px]" />
|
|
71
|
+
</Link>
|
|
72
|
+
) : (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
aria-label="Go to Dashboard"
|
|
76
|
+
onClick={() => onNavigate?.("dashboard")}
|
|
77
|
+
className="flex items-center justify-center transition-opacity hover:opacity-85 cursor-pointer"
|
|
78
|
+
>
|
|
79
|
+
<CastleIcon className="h-[36px] w-[36px] min-h-[36px] min-w-[36px] shrink-0 text-[var(--logo-color)] -mt-[3px]" />
|
|
80
|
+
</button>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Navigation */}
|
|
85
|
+
<nav className="flex-1 space-y-1 px-2">
|
|
86
|
+
{navItems.map((item) => {
|
|
87
|
+
const isActive = effectiveActive === item.id;
|
|
88
|
+
const NavEl = useLinks ? (
|
|
89
|
+
<Link
|
|
90
|
+
href={item.href}
|
|
91
|
+
className={cn(
|
|
92
|
+
"flex items-center justify-center w-full rounded-[20px] p-2.5 cursor-pointer",
|
|
93
|
+
isActive
|
|
94
|
+
? "bg-accent/10 text-accent"
|
|
95
|
+
: "text-foreground-secondary hover:text-foreground hover:bg-surface-hover"
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<item.icon className="h-5 w-5 shrink-0" />
|
|
99
|
+
</Link>
|
|
100
|
+
) : (
|
|
101
|
+
<button
|
|
102
|
+
onClick={() => onNavigate?.(item.id)}
|
|
103
|
+
className={cn(
|
|
104
|
+
"flex items-center justify-center w-full rounded-[20px] p-2.5 cursor-pointer",
|
|
105
|
+
isActive
|
|
106
|
+
? "bg-accent/10 text-accent"
|
|
107
|
+
: "text-foreground-secondary hover:text-foreground hover:bg-surface-hover"
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
<item.icon className="h-5 w-5 shrink-0" />
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Tooltip key={item.id} content={item.label} side="right">
|
|
116
|
+
{NavEl}
|
|
117
|
+
</Tooltip>
|
|
118
|
+
);
|
|
119
|
+
})}
|
|
120
|
+
</nav>
|
|
121
|
+
|
|
122
|
+
{/* Spacer at bottom for visual balance */}
|
|
123
|
+
<div className="pb-4" />
|
|
124
|
+
</aside>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { Sidebar };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTheme } from "next-themes";
|
|
4
|
+
import { Sun, Moon } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
|
|
8
|
+
export interface ThemeToggleProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
collapsed?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ThemeToggle({ className, collapsed = false }: ThemeToggleProps) {
|
|
14
|
+
const { theme, setTheme } = useTheme();
|
|
15
|
+
const [mounted, setMounted] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
setMounted(true);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
if (!mounted) {
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex items-center gap-3 p-2 rounded-[var(--radius-md)] hover:bg-surface transition-colors",
|
|
26
|
+
collapsed ? "justify-center" : "",
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<div className="h-5 w-5" />
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const isDark = theme === "dark";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => setTheme(isDark ? "light" : "dark")}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex items-center gap-3 p-2 rounded-[var(--radius-md)] hover:bg-surface transition-colors text-foreground-secondary hover:text-foreground",
|
|
42
|
+
collapsed ? "justify-center" : "",
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
title={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
|
46
|
+
>
|
|
47
|
+
{isDark ? (
|
|
48
|
+
<Sun className="h-5 w-5" />
|
|
49
|
+
) : (
|
|
50
|
+
<Moon className="h-5 w-5" />
|
|
51
|
+
)}
|
|
52
|
+
{!collapsed && (
|
|
53
|
+
<span className="text-sm">{isDark ? "Light mode" : "Dark mode"}</span>
|
|
54
|
+
)}
|
|
55
|
+
</button>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { ThemeToggle };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import { useTheme } from "next-themes";
|
|
5
|
+
import { User, Sun, Moon } from "lucide-react";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
export interface UserMenuProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
variant?: "glass" | "solid";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function UserMenu({ className, variant = "solid" }: UserMenuProps) {
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
const { theme, setTheme } = useTheme();
|
|
16
|
+
const [mounted, setMounted] = useState(false);
|
|
17
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setMounted(true);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
function handleClickOutside(event: MouseEvent) {
|
|
25
|
+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
26
|
+
setOpen(false);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
30
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const isDark = theme === "dark";
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div ref={menuRef} className={cn("relative", className)}>
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => setOpen(!open)}
|
|
39
|
+
className={cn(
|
|
40
|
+
"flex items-center justify-center h-14 w-14 rounded-[28px] shadow-xl shadow-black/20 text-foreground-secondary hover:text-foreground cursor-pointer",
|
|
41
|
+
variant === "glass" ? "glass" : "bg-surface border border-border"
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
<User className="h-5 w-5" />
|
|
45
|
+
</button>
|
|
46
|
+
|
|
47
|
+
{open && (
|
|
48
|
+
<div className="absolute right-0 top-[calc(100%+8px)] w-48 rounded-[var(--radius-md)] bg-surface border border-border shadow-xl py-1 z-50">
|
|
49
|
+
{mounted && (
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => {
|
|
52
|
+
setTheme(isDark ? "light" : "dark");
|
|
53
|
+
setOpen(false);
|
|
54
|
+
}}
|
|
55
|
+
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-foreground-secondary hover:text-foreground hover:bg-surface-hover cursor-pointer"
|
|
56
|
+
>
|
|
57
|
+
{isDark ? (
|
|
58
|
+
<Sun className="h-4 w-4" />
|
|
59
|
+
) : (
|
|
60
|
+
<Moon className="h-4 w-4" />
|
|
61
|
+
)}
|
|
62
|
+
{isDark ? "Light mode" : "Dark mode"}
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export { UserMenu };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { AlertCircle, CheckCircle, Info, AlertTriangle, X } from "lucide-react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface AlertProps extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
variant?: "info" | "success" | "warning" | "error";
|
|
7
|
+
dismissible?: boolean;
|
|
8
|
+
onDismiss?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const variantStyles = {
|
|
12
|
+
info: {
|
|
13
|
+
container: "bg-info/10 border-info/20 text-info",
|
|
14
|
+
icon: Info,
|
|
15
|
+
},
|
|
16
|
+
success: {
|
|
17
|
+
container: "bg-success/10 border-success/20 text-success",
|
|
18
|
+
icon: CheckCircle,
|
|
19
|
+
},
|
|
20
|
+
warning: {
|
|
21
|
+
container: "bg-warning/10 border-warning/20 text-warning",
|
|
22
|
+
icon: AlertTriangle,
|
|
23
|
+
},
|
|
24
|
+
error: {
|
|
25
|
+
container: "bg-error/10 border-error/20 text-error",
|
|
26
|
+
icon: AlertCircle,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
className,
|
|
34
|
+
variant = "info",
|
|
35
|
+
dismissible = false,
|
|
36
|
+
onDismiss,
|
|
37
|
+
children,
|
|
38
|
+
...props
|
|
39
|
+
},
|
|
40
|
+
ref
|
|
41
|
+
) => {
|
|
42
|
+
const { container, icon: Icon } = variantStyles[variant];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={cn(
|
|
47
|
+
"flex items-start gap-3 rounded-[var(--radius-md)] border p-4",
|
|
48
|
+
container,
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
ref={ref}
|
|
52
|
+
role="alert"
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
<Icon className="h-5 w-5 shrink-0 mt-0.5" />
|
|
56
|
+
<div className="flex-1 text-sm">{children}</div>
|
|
57
|
+
{dismissible && onDismiss && (
|
|
58
|
+
<button
|
|
59
|
+
onClick={onDismiss}
|
|
60
|
+
className="shrink-0 p-1 rounded-[var(--radius-sm)] hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
|
|
61
|
+
>
|
|
62
|
+
<X className="h-4 w-4" />
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
Alert.displayName = "Alert";
|
|
71
|
+
|
|
72
|
+
export { Alert };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface AvatarProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
size?: "sm" | "md" | "lg";
|
|
6
|
+
status?: "online" | "offline" | "busy" | "away";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
|
|
10
|
+
({ className, size = "md", status, children, ...props }, ref) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className="relative inline-block">
|
|
13
|
+
<div
|
|
14
|
+
className={cn(
|
|
15
|
+
"relative flex shrink-0 overflow-hidden rounded-[var(--radius-full)] bg-surface border border-border",
|
|
16
|
+
{
|
|
17
|
+
"h-8 w-8": size === "sm",
|
|
18
|
+
"h-10 w-10": size === "md",
|
|
19
|
+
"h-12 w-12": size === "lg",
|
|
20
|
+
},
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
ref={ref}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
{status && (
|
|
29
|
+
<span
|
|
30
|
+
className={cn(
|
|
31
|
+
"absolute bottom-0 right-0 block rounded-[var(--radius-full)] ring-2 ring-background",
|
|
32
|
+
{
|
|
33
|
+
"h-2.5 w-2.5": size === "sm",
|
|
34
|
+
"h-3 w-3": size === "md",
|
|
35
|
+
"h-3.5 w-3.5": size === "lg",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"bg-success": status === "online",
|
|
39
|
+
"bg-foreground-muted": status === "offline",
|
|
40
|
+
"bg-error": status === "busy",
|
|
41
|
+
"bg-warning": status === "away",
|
|
42
|
+
}
|
|
43
|
+
)}
|
|
44
|
+
/>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
Avatar.displayName = "Avatar";
|
|
52
|
+
|
|
53
|
+
const AvatarImage = forwardRef<
|
|
54
|
+
HTMLImageElement,
|
|
55
|
+
React.ImgHTMLAttributes<HTMLImageElement>
|
|
56
|
+
>(({ className, alt, ...props }, ref) => {
|
|
57
|
+
return (
|
|
58
|
+
<img
|
|
59
|
+
className={cn("aspect-square h-full w-full object-cover", className)}
|
|
60
|
+
alt={alt}
|
|
61
|
+
ref={ref}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
AvatarImage.displayName = "AvatarImage";
|
|
68
|
+
|
|
69
|
+
const AvatarFallback = forwardRef<
|
|
70
|
+
HTMLSpanElement,
|
|
71
|
+
HTMLAttributes<HTMLSpanElement>
|
|
72
|
+
>(({ className, ...props }, ref) => {
|
|
73
|
+
return (
|
|
74
|
+
<span
|
|
75
|
+
className={cn(
|
|
76
|
+
"flex h-full w-full items-center justify-center bg-surface text-foreground-secondary font-medium text-sm",
|
|
77
|
+
className
|
|
78
|
+
)}
|
|
79
|
+
ref={ref}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
AvatarFallback.displayName = "AvatarFallback";
|
|
86
|
+
|
|
87
|
+
export { Avatar, AvatarImage, AvatarFallback };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
5
|
+
variant?: "default" | "success" | "warning" | "error" | "info" | "outline";
|
|
6
|
+
size?: "sm" | "md";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function Badge({
|
|
10
|
+
className,
|
|
11
|
+
variant = "default",
|
|
12
|
+
size = "md",
|
|
13
|
+
...props
|
|
14
|
+
}: BadgeProps) {
|
|
15
|
+
return (
|
|
16
|
+
<span
|
|
17
|
+
className={cn(
|
|
18
|
+
"inline-flex items-center font-medium rounded-[var(--radius-full)] transition-colors",
|
|
19
|
+
{
|
|
20
|
+
"bg-surface text-foreground-secondary": variant === "default",
|
|
21
|
+
"bg-success/10 text-success": variant === "success",
|
|
22
|
+
"bg-warning/10 text-warning": variant === "warning",
|
|
23
|
+
"bg-error/10 text-error": variant === "error",
|
|
24
|
+
"bg-info/10 text-info": variant === "info",
|
|
25
|
+
"bg-transparent text-foreground-secondary border border-border":
|
|
26
|
+
variant === "outline",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"px-2 py-0.5 text-xs": size === "sm",
|
|
30
|
+
"px-2.5 py-0.5 text-sm": size === "md",
|
|
31
|
+
},
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { Badge };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { forwardRef, type ButtonHTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
variant?: "primary" | "secondary" | "ghost" | "destructive" | "outline";
|
|
6
|
+
size?: "sm" | "md" | "lg" | "icon";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
10
|
+
({ className, variant = "primary", size = "md", ...props }, ref) => {
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
className={cn(
|
|
14
|
+
"inline-flex items-center justify-center font-medium transition-colors rounded-[var(--radius-sm)] interactive disabled:pointer-events-none",
|
|
15
|
+
{
|
|
16
|
+
"bg-accent text-accent-foreground hover:bg-accent-hover":
|
|
17
|
+
variant === "primary",
|
|
18
|
+
"bg-surface text-foreground border border-border hover:bg-surface-hover hover:border-border-hover":
|
|
19
|
+
variant === "secondary",
|
|
20
|
+
"bg-transparent text-foreground hover:bg-surface":
|
|
21
|
+
variant === "ghost",
|
|
22
|
+
"bg-error text-white hover:bg-error/90": variant === "destructive",
|
|
23
|
+
"bg-transparent text-foreground border border-border hover:bg-surface hover:border-border-hover":
|
|
24
|
+
variant === "outline",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"h-8 px-3 text-sm": size === "sm",
|
|
28
|
+
"h-10 px-4 text-sm": size === "md",
|
|
29
|
+
"h-12 px-6 text-base": size === "lg",
|
|
30
|
+
"h-10 w-10 p-0": size === "icon",
|
|
31
|
+
},
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
ref={ref}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
Button.displayName = "Button";
|
|
42
|
+
|
|
43
|
+
export { Button };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
variant?: "default" | "bordered" | "elevated";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const Card = forwardRef<HTMLDivElement, CardProps>(
|
|
9
|
+
({ className, variant = "default", ...props }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className={cn(
|
|
13
|
+
"rounded-[var(--radius-md)] bg-surface p-6",
|
|
14
|
+
{
|
|
15
|
+
"": variant === "default",
|
|
16
|
+
"border border-border": variant === "bordered",
|
|
17
|
+
"shadow-lg shadow-black/5 dark:shadow-black/20":
|
|
18
|
+
variant === "elevated",
|
|
19
|
+
},
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
ref={ref}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
Card.displayName = "Card";
|
|
30
|
+
|
|
31
|
+
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
32
|
+
({ className, ...props }, ref) => {
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
className={cn("flex flex-col space-y-1.5 pb-4", className)}
|
|
36
|
+
ref={ref}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CardHeader.displayName = "CardHeader";
|
|
44
|
+
|
|
45
|
+
const CardTitle = forwardRef<
|
|
46
|
+
HTMLHeadingElement,
|
|
47
|
+
HTMLAttributes<HTMLHeadingElement>
|
|
48
|
+
>(({ className, ...props }, ref) => {
|
|
49
|
+
return (
|
|
50
|
+
<h3
|
|
51
|
+
className={cn(
|
|
52
|
+
"text-lg font-semibold leading-none tracking-tight text-foreground",
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
ref={ref}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
CardTitle.displayName = "CardTitle";
|
|
62
|
+
|
|
63
|
+
const CardDescription = forwardRef<
|
|
64
|
+
HTMLParagraphElement,
|
|
65
|
+
HTMLAttributes<HTMLParagraphElement>
|
|
66
|
+
>(({ className, ...props }, ref) => {
|
|
67
|
+
return (
|
|
68
|
+
<p
|
|
69
|
+
className={cn("text-sm text-foreground-secondary", className)}
|
|
70
|
+
ref={ref}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
CardDescription.displayName = "CardDescription";
|
|
77
|
+
|
|
78
|
+
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
79
|
+
({ className, ...props }, ref) => {
|
|
80
|
+
return <div className={cn("", className)} ref={ref} {...props} />;
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CardContent.displayName = "CardContent";
|
|
85
|
+
|
|
86
|
+
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
87
|
+
({ className, ...props }, ref) => {
|
|
88
|
+
return (
|
|
89
|
+
<div
|
|
90
|
+
className={cn("flex items-center pt-4", className)}
|
|
91
|
+
ref={ref}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CardFooter.displayName = "CardFooter";
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
Card,
|
|
102
|
+
CardHeader,
|
|
103
|
+
CardTitle,
|
|
104
|
+
CardDescription,
|
|
105
|
+
CardContent,
|
|
106
|
+
CardFooter,
|
|
107
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type ButtonHTMLAttributes } from "react";
|
|
4
|
+
import { Check } from "lucide-react";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
export interface CheckboxProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
|
+
checked?: boolean;
|
|
9
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
|
14
|
+
({ className, checked = false, onCheckedChange, label, ...props }, ref) => {
|
|
15
|
+
const checkbox = (
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
role="checkbox"
|
|
19
|
+
aria-checked={checked}
|
|
20
|
+
data-state={checked ? "checked" : "unchecked"}
|
|
21
|
+
onClick={() => onCheckedChange?.(!checked)}
|
|
22
|
+
className={cn(
|
|
23
|
+
"peer h-5 w-5 shrink-0 rounded-[var(--radius-sm)] border-2 transition-colors interactive",
|
|
24
|
+
checked
|
|
25
|
+
? "bg-accent border-accent text-accent-foreground"
|
|
26
|
+
: "bg-[var(--input-background)] border-[var(--input-border)]",
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
ref={ref}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
{checked && <Check className="h-4 w-4 mx-auto" strokeWidth={3} />}
|
|
33
|
+
</button>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (label) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex items-center gap-3">
|
|
39
|
+
{checkbox}
|
|
40
|
+
<span
|
|
41
|
+
className="text-sm text-foreground select-none cursor-pointer"
|
|
42
|
+
onClick={() => onCheckedChange?.(!checked)}
|
|
43
|
+
>
|
|
44
|
+
{label}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return checkbox;
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
Checkbox.displayName = "Checkbox";
|
|
55
|
+
|
|
56
|
+
export { Checkbox };
|