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