@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,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 };