@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,109 @@
1
+ "use client";
2
+
3
+ import { forwardRef, createContext, useContext, type HTMLAttributes } from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ interface RadioGroupContextValue {
7
+ value: string;
8
+ onValueChange: (value: string) => void;
9
+ name: string;
10
+ }
11
+
12
+ const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);
13
+
14
+ export interface RadioGroupProps extends HTMLAttributes<HTMLDivElement> {
15
+ value: string;
16
+ onValueChange: (value: string) => void;
17
+ name?: string;
18
+ }
19
+
20
+ const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
21
+ ({ className, value, onValueChange, name = "radio-group", children, ...props }, ref) => {
22
+ return (
23
+ <RadioGroupContext.Provider value={{ value, onValueChange, name }}>
24
+ <div
25
+ ref={ref}
26
+ role="radiogroup"
27
+ className={cn("flex flex-col gap-3", className)}
28
+ {...props}
29
+ >
30
+ {children}
31
+ </div>
32
+ </RadioGroupContext.Provider>
33
+ );
34
+ }
35
+ );
36
+
37
+ RadioGroup.displayName = "RadioGroup";
38
+
39
+ export interface RadioGroupItemProps extends HTMLAttributes<HTMLButtonElement> {
40
+ value: string;
41
+ label?: string;
42
+ disabled?: boolean;
43
+ }
44
+
45
+ const RadioGroupItem = forwardRef<HTMLButtonElement, RadioGroupItemProps>(
46
+ ({ className, value, label, disabled = false, ...props }, ref) => {
47
+ const context = useContext(RadioGroupContext);
48
+
49
+ if (!context) {
50
+ throw new Error("RadioGroupItem must be used within a RadioGroup");
51
+ }
52
+
53
+ const isSelected = context.value === value;
54
+
55
+ const handleClick = () => {
56
+ if (!disabled) {
57
+ context.onValueChange(value);
58
+ }
59
+ };
60
+
61
+ const radio = (
62
+ <button
63
+ ref={ref}
64
+ type="button"
65
+ role="radio"
66
+ aria-checked={isSelected}
67
+ data-state={isSelected ? "checked" : "unchecked"}
68
+ disabled={disabled}
69
+ onClick={handleClick}
70
+ className={cn(
71
+ "h-5 w-5 shrink-0 rounded-full border-2 transition-colors interactive",
72
+ "flex items-center justify-center",
73
+ isSelected
74
+ ? "border-accent"
75
+ : "border-[var(--input-border)] bg-[var(--input-background)]",
76
+ className
77
+ )}
78
+ {...props}
79
+ >
80
+ {isSelected && (
81
+ <span className="h-2.5 w-2.5 rounded-full bg-accent" />
82
+ )}
83
+ </button>
84
+ );
85
+
86
+ if (label) {
87
+ return (
88
+ <div className="flex items-center gap-3">
89
+ {radio}
90
+ <span
91
+ className={cn(
92
+ "text-sm select-none cursor-pointer",
93
+ disabled ? "text-foreground-muted" : "text-foreground"
94
+ )}
95
+ onClick={handleClick}
96
+ >
97
+ {label}
98
+ </span>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ return radio;
104
+ }
105
+ );
106
+
107
+ RadioGroupItem.displayName = "RadioGroupItem";
108
+
109
+ export { RadioGroup, RadioGroupItem };
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type SelectHTMLAttributes } from "react";
4
+ import { ChevronDown } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
8
+ error?: boolean;
9
+ label?: string;
10
+ }
11
+
12
+ const Select = forwardRef<HTMLSelectElement, SelectProps>(
13
+ ({ className, error, label, id, children, ...props }, ref) => {
14
+ const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
15
+
16
+ return (
17
+ <div className="w-full">
18
+ {label && (
19
+ <label htmlFor={inputId} className="form-label">
20
+ {label}
21
+ </label>
22
+ )}
23
+ <div className="relative">
24
+ <select
25
+ id={inputId}
26
+ className={cn(
27
+ "input-base flex appearance-none pr-10 cursor-pointer",
28
+ error && "error",
29
+ className
30
+ )}
31
+ aria-invalid={error}
32
+ ref={ref}
33
+ {...props}
34
+ >
35
+ {children}
36
+ </select>
37
+ <ChevronDown className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-foreground-muted pointer-events-none" />
38
+ </div>
39
+ </div>
40
+ );
41
+ }
42
+ );
43
+
44
+ Select.displayName = "Select";
45
+
46
+ export { Select };
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { forwardRef, useState } from "react";
4
+ import * as SliderPrimitive from "@radix-ui/react-slider";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export interface SliderProps
8
+ extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
9
+ label?: string;
10
+ showValue?: boolean;
11
+ }
12
+
13
+ const Slider = forwardRef<
14
+ React.ElementRef<typeof SliderPrimitive.Root>,
15
+ SliderProps
16
+ >(({ className, label, showValue = false, value, defaultValue, onValueChange, ...props }, ref) => {
17
+ const [internalValue, setInternalValue] = useState(defaultValue ?? [0]);
18
+
19
+ const displayValue = value ?? internalValue;
20
+
21
+ const handleValueChange = (newValue: number[]) => {
22
+ setInternalValue(newValue);
23
+ onValueChange?.(newValue);
24
+ };
25
+
26
+ return (
27
+ <div className="w-full">
28
+ {(label || showValue) && (
29
+ <div className="flex items-center justify-between mb-3">
30
+ {label && (
31
+ <label className="text-sm text-foreground-muted">{label}</label>
32
+ )}
33
+ {showValue && (
34
+ <span className="text-sm text-foreground tabular-nums">
35
+ {displayValue[0]}
36
+ </span>
37
+ )}
38
+ </div>
39
+ )}
40
+ <SliderPrimitive.Root
41
+ ref={ref}
42
+ className={cn(
43
+ "relative flex w-full touch-none select-none items-center",
44
+ className
45
+ )}
46
+ value={value}
47
+ defaultValue={defaultValue}
48
+ onValueChange={handleValueChange}
49
+ {...props}
50
+ >
51
+ <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-border">
52
+ <SliderPrimitive.Range className="absolute h-full bg-accent" />
53
+ </SliderPrimitive.Track>
54
+ <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-accent bg-white shadow-md transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 hover:scale-110" />
55
+ </SliderPrimitive.Root>
56
+ </div>
57
+ );
58
+ });
59
+
60
+ Slider.displayName = "Slider";
61
+
62
+ export { Slider };
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { forwardRef, createContext, useContext, type HTMLAttributes } from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ interface TabsContextValue {
7
+ value: string;
8
+ onValueChange: (value: string) => void;
9
+ }
10
+
11
+ const TabsContext = createContext<TabsContextValue | null>(null);
12
+
13
+ export interface TabsProps extends HTMLAttributes<HTMLDivElement> {
14
+ value: string;
15
+ onValueChange: (value: string) => void;
16
+ }
17
+
18
+ const Tabs = forwardRef<HTMLDivElement, TabsProps>(
19
+ ({ className, value, onValueChange, children, ...props }, ref) => {
20
+ return (
21
+ <TabsContext.Provider value={{ value, onValueChange }}>
22
+ <div ref={ref} className={cn("w-full", className)} {...props}>
23
+ {children}
24
+ </div>
25
+ </TabsContext.Provider>
26
+ );
27
+ }
28
+ );
29
+
30
+ Tabs.displayName = "Tabs";
31
+
32
+ export type TabsListProps = HTMLAttributes<HTMLDivElement>;
33
+
34
+ const TabsList = forwardRef<HTMLDivElement, TabsListProps>(
35
+ ({ className, children, ...props }, ref) => {
36
+ return (
37
+ <div
38
+ ref={ref}
39
+ role="tablist"
40
+ className={cn(
41
+ "inline-flex items-center gap-1 border-b border-border",
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ </div>
48
+ );
49
+ }
50
+ );
51
+
52
+ TabsList.displayName = "TabsList";
53
+
54
+ export interface TabsTriggerProps extends HTMLAttributes<HTMLButtonElement> {
55
+ value: string;
56
+ disabled?: boolean;
57
+ }
58
+
59
+ const TabsTrigger = forwardRef<HTMLButtonElement, TabsTriggerProps>(
60
+ ({ className, value, disabled = false, children, ...props }, ref) => {
61
+ const context = useContext(TabsContext);
62
+
63
+ if (!context) {
64
+ throw new Error("TabsTrigger must be used within Tabs");
65
+ }
66
+
67
+ const isSelected = context.value === value;
68
+
69
+ return (
70
+ <button
71
+ ref={ref}
72
+ type="button"
73
+ role="tab"
74
+ aria-selected={isSelected}
75
+ data-state={isSelected ? "active" : "inactive"}
76
+ disabled={disabled}
77
+ onClick={() => !disabled && context.onValueChange(value)}
78
+ className={cn(
79
+ "relative px-4 py-2.5 text-sm font-medium transition-colors cursor-pointer",
80
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2",
81
+ "disabled:pointer-events-none disabled:opacity-50",
82
+ isSelected
83
+ ? "text-foreground"
84
+ : "text-foreground-muted hover:text-foreground",
85
+ className
86
+ )}
87
+ {...props}
88
+ >
89
+ {children}
90
+ {isSelected && (
91
+ <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent" />
92
+ )}
93
+ </button>
94
+ );
95
+ }
96
+ );
97
+
98
+ TabsTrigger.displayName = "TabsTrigger";
99
+
100
+ export interface TabsContentProps extends HTMLAttributes<HTMLDivElement> {
101
+ value: string;
102
+ }
103
+
104
+ const TabsContent = forwardRef<HTMLDivElement, TabsContentProps>(
105
+ ({ className, value, children, ...props }, ref) => {
106
+ const context = useContext(TabsContext);
107
+
108
+ if (!context) {
109
+ throw new Error("TabsContent must be used within Tabs");
110
+ }
111
+
112
+ if (context.value !== value) {
113
+ return null;
114
+ }
115
+
116
+ return (
117
+ <div
118
+ ref={ref}
119
+ role="tabpanel"
120
+ tabIndex={0}
121
+ className={cn("mt-4 focus-visible:outline-none", className)}
122
+ {...props}
123
+ >
124
+ {children}
125
+ </div>
126
+ );
127
+ }
128
+ );
129
+
130
+ TabsContent.displayName = "TabsContent";
131
+
132
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { forwardRef, createContext, useContext, type HTMLAttributes, type ReactNode } from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ interface ToggleGroupContextValue {
7
+ value: string;
8
+ onValueChange: (value: string) => void;
9
+ }
10
+
11
+ const ToggleGroupContext = createContext<ToggleGroupContextValue | null>(null);
12
+
13
+ export interface ToggleGroupProps extends HTMLAttributes<HTMLDivElement> {
14
+ value: string;
15
+ onValueChange: (value: string) => void;
16
+ }
17
+
18
+ const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
19
+ ({ className, value, onValueChange, children, ...props }, ref) => {
20
+ return (
21
+ <ToggleGroupContext.Provider value={{ value, onValueChange }}>
22
+ <div
23
+ ref={ref}
24
+ role="group"
25
+ className={cn(
26
+ "inline-flex items-center rounded-[var(--radius-sm)] bg-surface border border-border p-1 gap-1",
27
+ className
28
+ )}
29
+ {...props}
30
+ >
31
+ {children}
32
+ </div>
33
+ </ToggleGroupContext.Provider>
34
+ );
35
+ }
36
+ );
37
+
38
+ ToggleGroup.displayName = "ToggleGroup";
39
+
40
+ export interface ToggleGroupItemProps extends HTMLAttributes<HTMLButtonElement> {
41
+ value: string;
42
+ disabled?: boolean;
43
+ children: ReactNode;
44
+ }
45
+
46
+ const ToggleGroupItem = forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
47
+ ({ className, value, disabled = false, children, ...props }, ref) => {
48
+ const context = useContext(ToggleGroupContext);
49
+
50
+ if (!context) {
51
+ throw new Error("ToggleGroupItem must be used within a ToggleGroup");
52
+ }
53
+
54
+ const isSelected = context.value === value;
55
+
56
+ return (
57
+ <button
58
+ ref={ref}
59
+ type="button"
60
+ role="radio"
61
+ aria-checked={isSelected}
62
+ data-state={isSelected ? "on" : "off"}
63
+ disabled={disabled}
64
+ onClick={() => !disabled && context.onValueChange(value)}
65
+ className={cn(
66
+ "inline-flex items-center justify-center h-8 px-3 rounded-[var(--radius-sm)] text-sm font-medium transition-all",
67
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2",
68
+ "disabled:pointer-events-none disabled:opacity-50",
69
+ "cursor-pointer",
70
+ isSelected
71
+ ? "bg-background text-foreground shadow-sm"
72
+ : "text-foreground-secondary hover:text-foreground hover:bg-surface-hover",
73
+ className
74
+ )}
75
+ {...props}
76
+ >
77
+ {children}
78
+ </button>
79
+ );
80
+ }
81
+ );
82
+
83
+ ToggleGroupItem.displayName = "ToggleGroupItem";
84
+
85
+ export { ToggleGroup, ToggleGroupItem };
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { forwardRef, type ButtonHTMLAttributes } from "react";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ export interface ToggleProps extends ButtonHTMLAttributes<HTMLButtonElement> {
7
+ pressed?: boolean;
8
+ onPressedChange?: (pressed: boolean) => void;
9
+ size?: "sm" | "md" | "lg";
10
+ label?: string;
11
+ }
12
+
13
+ const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(
14
+ (
15
+ { className, pressed = false, onPressedChange, size = "md", label, ...props },
16
+ ref
17
+ ) => {
18
+ const toggle = (
19
+ <button
20
+ type="button"
21
+ role="switch"
22
+ aria-checked={pressed}
23
+ data-state={pressed ? "on" : "off"}
24
+ onClick={() => onPressedChange?.(!pressed)}
25
+ className={cn(
26
+ "relative inline-flex shrink-0 items-center rounded-[var(--radius-full)] border-2 border-transparent transition-colors interactive",
27
+ pressed ? "bg-accent" : "bg-[var(--input-border)]",
28
+ {
29
+ "h-5 w-9": size === "sm",
30
+ "h-6 w-11": size === "md",
31
+ "h-7 w-14": size === "lg",
32
+ },
33
+ className
34
+ )}
35
+ ref={ref}
36
+ {...props}
37
+ >
38
+ <span
39
+ className={cn(
40
+ "pointer-events-none block rounded-[var(--radius-full)] bg-white shadow-lg ring-0 transition-transform",
41
+ {
42
+ "h-4 w-4": size === "sm",
43
+ "h-5 w-5": size === "md",
44
+ "h-6 w-6": size === "lg",
45
+ },
46
+ pressed
47
+ ? {
48
+ "translate-x-4": size === "sm",
49
+ "translate-x-5": size === "md",
50
+ "translate-x-7": size === "lg",
51
+ }
52
+ : "translate-x-0"
53
+ )}
54
+ />
55
+ </button>
56
+ );
57
+
58
+ if (label) {
59
+ return (
60
+ <div className="flex items-center gap-3">
61
+ {toggle}
62
+ <span
63
+ className="text-sm text-foreground select-none cursor-pointer"
64
+ onClick={() => onPressedChange?.(!pressed)}
65
+ >
66
+ {label}
67
+ </span>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ return toggle;
73
+ }
74
+ );
75
+
76
+ Toggle.displayName = "Toggle";
77
+
78
+ export { Toggle };
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useLayoutEffect, useEffect, useCallback, type ReactNode } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export interface TooltipProps {
8
+ children: ReactNode;
9
+ content: string;
10
+ side?: "top" | "right" | "bottom" | "left";
11
+ className?: string;
12
+ }
13
+
14
+ function Tooltip({
15
+ children,
16
+ content,
17
+ side = "right",
18
+ className
19
+ }: TooltipProps) {
20
+ const [isHovered, setIsHovered] = useState(false);
21
+ const [mounted, setMounted] = useState(false);
22
+ const [position, setPosition] = useState({ x: 0, y: 0 });
23
+ const [isPositioned, setIsPositioned] = useState(false);
24
+ const triggerRef = useRef<HTMLDivElement>(null);
25
+ const tooltipRef = useRef<HTMLDivElement>(null);
26
+
27
+ useEffect(() => {
28
+ setMounted(true);
29
+ }, []);
30
+
31
+ const updatePosition = useCallback(() => {
32
+ if (triggerRef.current && tooltipRef.current) {
33
+ const triggerRect = triggerRef.current.getBoundingClientRect();
34
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
35
+ const gap = 10;
36
+
37
+ let x = 0;
38
+ let y = 0;
39
+
40
+ switch (side) {
41
+ case "top":
42
+ x = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
43
+ y = triggerRect.top - tooltipRect.height - gap;
44
+ break;
45
+ case "bottom":
46
+ x = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
47
+ y = triggerRect.bottom + gap;
48
+ break;
49
+ case "left":
50
+ x = triggerRect.left - tooltipRect.width - gap;
51
+ y = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
52
+ break;
53
+ case "right":
54
+ default:
55
+ x = triggerRect.right + gap;
56
+ y = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
57
+ break;
58
+ }
59
+
60
+ setPosition({ x, y });
61
+ setIsPositioned(true);
62
+ }
63
+ }, [side]);
64
+
65
+ useLayoutEffect(() => {
66
+ if (!isHovered) {
67
+ setIsPositioned(false);
68
+ return;
69
+ }
70
+
71
+ updatePosition();
72
+
73
+ const onChange = () => updatePosition();
74
+ window.addEventListener("resize", onChange);
75
+ window.addEventListener("scroll", onChange, true);
76
+ return () => {
77
+ window.removeEventListener("resize", onChange);
78
+ window.removeEventListener("scroll", onChange, true);
79
+ };
80
+ }, [isHovered, updatePosition]);
81
+
82
+ return (
83
+ <>
84
+ <div
85
+ ref={triggerRef}
86
+ onMouseEnter={() => setIsHovered(true)}
87
+ onMouseLeave={() => setIsHovered(false)}
88
+ className={className}
89
+ >
90
+ {children}
91
+ </div>
92
+ {isHovered && mounted
93
+ ? createPortal(
94
+ <div
95
+ ref={tooltipRef}
96
+ className="fixed z-[9999] pointer-events-none"
97
+ style={{
98
+ left: position.x,
99
+ top: position.y,
100
+ visibility: isPositioned ? "visible" : "hidden",
101
+ }}
102
+ >
103
+ <div
104
+ className={cn(
105
+ "transition-all duration-150 ease-out",
106
+ isPositioned ? "opacity-100 scale-100" : "opacity-0 scale-95",
107
+ side === "right" && (isPositioned ? "translate-x-0" : "-translate-x-2"),
108
+ side === "left" && (isPositioned ? "translate-x-0" : "translate-x-2"),
109
+ side === "top" && (isPositioned ? "translate-y-0" : "translate-y-2"),
110
+ side === "bottom" && (isPositioned ? "translate-y-0" : "-translate-y-2")
111
+ )}
112
+ >
113
+ <div className="relative bg-foreground text-background text-sm font-medium px-3 py-1.5 rounded-[4px] whitespace-nowrap shadow-xl shadow-black/25">
114
+ {content}
115
+ {side === "right" && (
116
+ <div className="absolute -left-[7px] top-1/2 -translate-y-1/2">
117
+ <div className="w-0 h-0 border-t-[8px] border-t-transparent border-b-[8px] border-b-transparent border-r-[8px] border-r-foreground" />
118
+ </div>
119
+ )}
120
+ {side === "left" && (
121
+ <div className="absolute -right-[7px] top-1/2 -translate-y-1/2">
122
+ <div className="w-0 h-0 border-t-[8px] border-t-transparent border-b-[8px] border-b-transparent border-l-[8px] border-l-foreground" />
123
+ </div>
124
+ )}
125
+ {side === "top" && (
126
+ <div className="absolute -bottom-[7px] left-1/2 -translate-x-1/2">
127
+ <div className="w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-t-[8px] border-t-foreground" />
128
+ </div>
129
+ )}
130
+ {side === "bottom" && (
131
+ <div className="absolute -top-[7px] left-1/2 -translate-x-1/2">
132
+ <div className="w-0 h-0 border-l-[8px] border-l-transparent border-r-[8px] border-r-transparent border-b-[8px] border-b-foreground" />
133
+ </div>
134
+ )}
135
+ </div>
136
+ </div>
137
+ </div>,
138
+ document.body
139
+ )
140
+ : null}
141
+ </>
142
+ );
143
+ }
144
+
145
+ export { Tooltip };