@swift-rust/ui 0.2.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/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +437 -0
- package/dist/cli.js.map +1 -0
- package/dist/components.d.ts +8 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +39 -0
- package/dist/components.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/smoke.test.d.ts +2 -0
- package/dist/smoke.test.d.ts.map +1 -0
- package/dist/smoke.test.js +18 -0
- package/dist/smoke.test.js.map +1 -0
- package/package.json +54 -0
- package/registry/components/accordion.tsx +106 -0
- package/registry/components/alert.tsx +34 -0
- package/registry/components/avatar.tsx +37 -0
- package/registry/components/badge.tsx +30 -0
- package/registry/components/breadcrumb.tsx +56 -0
- package/registry/components/button.tsx +48 -0
- package/registry/components/callout.tsx +20 -0
- package/registry/components/card.tsx +49 -0
- package/registry/components/checkbox.tsx +21 -0
- package/registry/components/code.tsx +27 -0
- package/registry/components/command.tsx +93 -0
- package/registry/components/dialog.tsx +118 -0
- package/registry/components/dropdown-menu.tsx +96 -0
- package/registry/components/form.tsx +28 -0
- package/registry/components/input.tsx +23 -0
- package/registry/components/kbd.tsx +17 -0
- package/registry/components/label.tsx +19 -0
- package/registry/components/navigation-menu.tsx +74 -0
- package/registry/components/pagination.tsx +56 -0
- package/registry/components/popover.tsx +58 -0
- package/registry/components/progress.tsx +22 -0
- package/registry/components/radio-group.tsx +78 -0
- package/registry/components/select.tsx +21 -0
- package/registry/components/separator.tsx +23 -0
- package/registry/components/sheet.tsx +68 -0
- package/registry/components/skeleton.tsx +14 -0
- package/registry/components/slider.tsx +18 -0
- package/registry/components/spinner.tsx +17 -0
- package/registry/components/switch.tsx +23 -0
- package/registry/components/table.tsx +65 -0
- package/registry/components/tabs.tsx +86 -0
- package/registry/components/textarea.tsx +22 -0
- package/registry/components/toast.tsx +58 -0
- package/registry/components/toggle.tsx +46 -0
- package/registry/components/tooltip.tsx +70 -0
- package/registry/lib/utils.ts +6 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const SheetContext = React.createContext<{ open: boolean; setOpen: (v: boolean) => void } | null>(null);
|
|
6
|
+
|
|
7
|
+
export function Sheet({
|
|
8
|
+
open: controlled,
|
|
9
|
+
onOpenChange,
|
|
10
|
+
side = "right",
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
open?: boolean;
|
|
14
|
+
onOpenChange?: (v: boolean) => void;
|
|
15
|
+
side?: "left" | "right" | "top" | "bottom";
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
}) {
|
|
18
|
+
const [internal, setInternal] = React.useState(false);
|
|
19
|
+
const open = controlled ?? internal;
|
|
20
|
+
const setOpen = (v: boolean) => {
|
|
21
|
+
if (controlled === undefined) setInternal(v);
|
|
22
|
+
onOpenChange?.(v);
|
|
23
|
+
};
|
|
24
|
+
const sideClass = {
|
|
25
|
+
right: "right-0 top-0 h-full w-3/4 max-w-sm border-l",
|
|
26
|
+
left: "left-0 top-0 h-full w-3/4 max-w-sm border-r",
|
|
27
|
+
top: "top-0 left-0 w-full h-1/3 max-h-sm border-b",
|
|
28
|
+
bottom: "bottom-0 left-0 w-full h-1/3 max-h-sm border-t",
|
|
29
|
+
}[side];
|
|
30
|
+
return (
|
|
31
|
+
<SheetContext.Provider value={{ open, setOpen }}>
|
|
32
|
+
{children}
|
|
33
|
+
{open ? (
|
|
34
|
+
<>
|
|
35
|
+
<div className="fixed inset-0 z-40 bg-black/60" onClick={() => setOpen(false)} aria-hidden />
|
|
36
|
+
<div
|
|
37
|
+
role="dialog"
|
|
38
|
+
className={cn(
|
|
39
|
+
"fixed z-50 bg-[var(--ui-surface)] p-6 shadow-lg transition-transform",
|
|
40
|
+
sideClass,
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{typeof children === "object" && children !== null && "type" in children ? null : children}
|
|
44
|
+
</div>
|
|
45
|
+
</>
|
|
46
|
+
) : null}
|
|
47
|
+
</SheetContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function SheetContent({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
52
|
+
return <div className={className}>{children}</div>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function SheetTrigger({ asChild, children }: { asChild?: boolean; children: React.ReactNode }) {
|
|
56
|
+
const ctx = React.useContext(SheetContext)!;
|
|
57
|
+
if (asChild && React.isValidElement(children)) {
|
|
58
|
+
return React.cloneElement(children as React.ReactElement<{ onClick?: React.MouseEventHandler }>, {
|
|
59
|
+
onClick: () => ctx.setOpen(true),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return <button type="button" onClick={() => ctx.setOpen(true)}>{children}</button>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function SheetClose({ children }: { children: React.ReactNode }) {
|
|
66
|
+
const ctx = React.useContext(SheetContext)!;
|
|
67
|
+
return <div onClick={() => ctx.setOpen(false)}>{children}</div>;
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Skeleton = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<div
|
|
8
|
+
ref={ref}
|
|
9
|
+
className={cn("animate-pulse rounded-md bg-[var(--ui-surface-2)]", className)}
|
|
10
|
+
{...props}
|
|
11
|
+
/>
|
|
12
|
+
),
|
|
13
|
+
);
|
|
14
|
+
Skeleton.displayName = "Skeleton";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Slider = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<input
|
|
8
|
+
type="range"
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
"h-1.5 w-full cursor-pointer appearance-none rounded-full bg-[var(--ui-surface-2)] accent-[var(--ui-accent)]",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
Slider.displayName = "Slider";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Spinner = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & { size?: number }>(
|
|
6
|
+
({ className, size = 16, ...props }, ref) => (
|
|
7
|
+
<div
|
|
8
|
+
ref={ref}
|
|
9
|
+
role="status"
|
|
10
|
+
aria-label="Loading"
|
|
11
|
+
className={cn("inline-block animate-spin rounded-full border-2 border-current border-t-transparent", className)}
|
|
12
|
+
style={{ width: size, height: size }}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
Spinner.displayName = "Spinner";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Switch = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<label className="inline-flex cursor-pointer items-center">
|
|
8
|
+
<input type="checkbox" ref={ref} className="peer sr-only" {...props} />
|
|
9
|
+
<span
|
|
10
|
+
className={cn(
|
|
11
|
+
"relative h-5 w-9 rounded-full bg-[var(--ui-surface-2)] transition-colors",
|
|
12
|
+
"peer-checked:bg-[var(--ui-accent)]",
|
|
13
|
+
"peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--ui-accent)] peer-focus-visible:ring-offset-2",
|
|
14
|
+
"peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
15
|
+
"after:absolute after:left-0.5 after:top-0.5 after:h-4 after:w-4 after:rounded-full after:bg-white after:transition-transform",
|
|
16
|
+
"peer-checked:after:translate-x-4",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
/>
|
|
20
|
+
</label>
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
Switch.displayName = "Switch";
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<div className="relative w-full overflow-auto">
|
|
8
|
+
<table
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
Table.displayName = "Table";
|
|
17
|
+
|
|
18
|
+
export const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
19
|
+
({ className, ...props }, ref) => (
|
|
20
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
TableHeader.displayName = "TableHeader";
|
|
24
|
+
|
|
25
|
+
export const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
26
|
+
({ className, ...props }, ref) => (
|
|
27
|
+
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
TableBody.displayName = "TableBody";
|
|
31
|
+
|
|
32
|
+
export const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
|
33
|
+
({ className, ...props }, ref) => (
|
|
34
|
+
<tr
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(
|
|
37
|
+
"border-b border-[var(--ui-border)] transition-colors hover:bg-[var(--ui-surface-2)] data-[state=selected]:bg-[var(--ui-surface-2)]",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
TableRow.displayName = "TableRow";
|
|
45
|
+
|
|
46
|
+
export const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
|
|
47
|
+
({ className, ...props }, ref) => (
|
|
48
|
+
<th
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn(
|
|
51
|
+
"h-10 px-3 text-left align-middle text-xs font-semibold uppercase tracking-wider text-[var(--ui-fg-muted)]",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
TableHead.displayName = "TableHead";
|
|
59
|
+
|
|
60
|
+
export const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
|
61
|
+
({ className, ...props }, ref) => (
|
|
62
|
+
<td ref={ref} className={cn("p-3 align-middle", className)} {...props} />
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
TableCell.displayName = "TableCell";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const TabsContext = React.createContext<{ value: string; setValue: (v: string) => void } | null>(null);
|
|
6
|
+
|
|
7
|
+
export function Tabs({
|
|
8
|
+
defaultValue,
|
|
9
|
+
value: controlled,
|
|
10
|
+
onValueChange,
|
|
11
|
+
className,
|
|
12
|
+
children,
|
|
13
|
+
}: {
|
|
14
|
+
defaultValue?: string;
|
|
15
|
+
value?: string;
|
|
16
|
+
onValueChange?: (v: string) => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
const [internal, setInternal] = React.useState(defaultValue ?? "");
|
|
21
|
+
const value = controlled ?? internal;
|
|
22
|
+
const setValue = (v: string) => {
|
|
23
|
+
if (controlled === undefined) setInternal(v);
|
|
24
|
+
onValueChange?.(v);
|
|
25
|
+
};
|
|
26
|
+
return (
|
|
27
|
+
<TabsContext.Provider value={{ value, setValue }}>
|
|
28
|
+
<div className={className}>{children}</div>
|
|
29
|
+
</TabsContext.Provider>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function TabsList({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
role="tablist"
|
|
37
|
+
className={cn(
|
|
38
|
+
"inline-flex h-9 items-center justify-center rounded-lg bg-[var(--ui-surface-2)] p-1 text-[var(--ui-fg-muted)]",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function TabsTrigger({
|
|
47
|
+
value,
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: { value: string } & React.HTMLAttributes<HTMLButtonElement>) {
|
|
52
|
+
const ctx = React.useContext(TabsContext)!;
|
|
53
|
+
const active = ctx.value === value;
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
role="tab"
|
|
58
|
+
aria-selected={active}
|
|
59
|
+
onClick={() => ctx.setValue(value)}
|
|
60
|
+
className={cn(
|
|
61
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all",
|
|
62
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-accent)]",
|
|
63
|
+
active ? "bg-[var(--ui-surface)] text-[var(--ui-fg)] shadow-sm" : "hover:text-[var(--ui-fg)]",
|
|
64
|
+
className,
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</button>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function TabsContent({
|
|
74
|
+
value,
|
|
75
|
+
className,
|
|
76
|
+
children,
|
|
77
|
+
...props
|
|
78
|
+
}: { value: string } & React.HTMLAttributes<HTMLDivElement>) {
|
|
79
|
+
const ctx = React.useContext(TabsContext)!;
|
|
80
|
+
if (ctx.value !== value) return null;
|
|
81
|
+
return (
|
|
82
|
+
<div role="tabpanel" className={cn("mt-2", className)} {...props}>
|
|
83
|
+
{children}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
|
6
|
+
|
|
7
|
+
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
8
|
+
({ className, ...props }, ref) => (
|
|
9
|
+
<textarea
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"flex min-h-[80px] w-full rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] px-3 py-2 text-sm shadow-sm",
|
|
13
|
+
"placeholder:text-[var(--ui-fg-subtle)]",
|
|
14
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-accent)] focus-visible:ring-offset-1",
|
|
15
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
Textarea.displayName = "Textarea";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
type ToastTone = "default" | "success" | "error" | "warning";
|
|
6
|
+
|
|
7
|
+
const ToastContext = React.createContext<{
|
|
8
|
+
push: (msg: string, tone?: ToastTone) => void;
|
|
9
|
+
} | null>(null);
|
|
10
|
+
|
|
11
|
+
interface Toast {
|
|
12
|
+
id: number;
|
|
13
|
+
message: string;
|
|
14
|
+
tone: ToastTone;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let counter = 0;
|
|
18
|
+
|
|
19
|
+
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
|
20
|
+
const [toasts, setToasts] = React.useState<Toast[]>([]);
|
|
21
|
+
const push = React.useCallback((message: string, tone: ToastTone = "default") => {
|
|
22
|
+
const id = ++counter;
|
|
23
|
+
setToasts((t) => [...t, { id, message, tone }]);
|
|
24
|
+
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 4000);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const TONE: Record<ToastTone, string> = {
|
|
28
|
+
default: "bg-[var(--ui-fg)] text-[var(--ui-bg)]",
|
|
29
|
+
success: "bg-[#16a34a] text-white",
|
|
30
|
+
error: "bg-[var(--ui-danger)] text-white",
|
|
31
|
+
warning: "bg-[#f59e0b] text-white",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ToastContext.Provider value={{ push }}>
|
|
36
|
+
{children}
|
|
37
|
+
<div className="pointer-events-none fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
|
38
|
+
{toasts.map((t) => (
|
|
39
|
+
<div
|
|
40
|
+
key={t.id}
|
|
41
|
+
className={cn(
|
|
42
|
+
"pointer-events-auto min-w-[200px] max-w-sm rounded-md px-4 py-2.5 text-sm shadow-lg fade-in",
|
|
43
|
+
TONE[t.tone],
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{t.message}
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</ToastContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useToast() {
|
|
55
|
+
const ctx = React.useContext(ToastContext);
|
|
56
|
+
if (!ctx) throw new Error("useToast must be used inside <ToastProvider>");
|
|
57
|
+
return ctx;
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Toggle = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement> & { pressed?: boolean }>(
|
|
6
|
+
({ className, pressed, ...props }, ref) => (
|
|
7
|
+
<button
|
|
8
|
+
ref={ref}
|
|
9
|
+
type="button"
|
|
10
|
+
aria-pressed={pressed}
|
|
11
|
+
data-state={pressed ? "on" : "off"}
|
|
12
|
+
className={cn(
|
|
13
|
+
"inline-flex h-9 items-center gap-2 rounded-md px-3 text-sm font-medium transition-colors",
|
|
14
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-accent)]",
|
|
15
|
+
pressed ? "bg-[var(--ui-surface-2)] text-[var(--ui-fg)]" : "hover:bg-[var(--ui-surface-2)]",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
Toggle.displayName = "Toggle";
|
|
23
|
+
|
|
24
|
+
export const ToggleGroup = ({
|
|
25
|
+
value,
|
|
26
|
+
onValueChange,
|
|
27
|
+
children,
|
|
28
|
+
className,
|
|
29
|
+
}: {
|
|
30
|
+
value: string;
|
|
31
|
+
onValueChange: (v: string) => void;
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
className?: string;
|
|
34
|
+
}) => (
|
|
35
|
+
<div role="group" className={cn("inline-flex rounded-md border border-[var(--ui-border)] p-0.5", className)}>
|
|
36
|
+
{React.Children.map(children, (child) => {
|
|
37
|
+
if (!React.isValidElement(child)) return child;
|
|
38
|
+
const c = child as React.ReactElement<{ value: string; pressed?: boolean; onClick?: () => void }>;
|
|
39
|
+
const active = c.props.value === value;
|
|
40
|
+
return React.cloneElement(c, {
|
|
41
|
+
pressed: active,
|
|
42
|
+
onClick: () => onValueChange(c.props.value),
|
|
43
|
+
});
|
|
44
|
+
})}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const TooltipContext = React.createContext<{ label: string | null; setLabel: (v: string | null) => void } | null>(
|
|
6
|
+
null,
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
export function TooltipProvider({ children }: { children: React.ReactNode }) {
|
|
10
|
+
const [label, setLabel] = React.useState<string | null>(null);
|
|
11
|
+
const [pos, setPos] = React.useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
12
|
+
return (
|
|
13
|
+
<TooltipContext.Provider value={{ label, setLabel }}>
|
|
14
|
+
{children}
|
|
15
|
+
{label ? (
|
|
16
|
+
<div
|
|
17
|
+
className="pointer-events-none fixed z-50 -translate-x-1/2 -translate-y-full rounded-md bg-[var(--ui-fg)] px-2 py-1 text-xs text-[var(--ui-bg)] shadow-md"
|
|
18
|
+
style={{ left: pos.x, top: pos.y - 6 }}
|
|
19
|
+
>
|
|
20
|
+
{label}
|
|
21
|
+
</div>
|
|
22
|
+
) : null}
|
|
23
|
+
<TooltipListener pos={pos} setPos={setPos} setLabel={setLabel} />
|
|
24
|
+
</TooltipContext.Provider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function TooltipListener({
|
|
29
|
+
pos,
|
|
30
|
+
setPos,
|
|
31
|
+
setLabel,
|
|
32
|
+
}: {
|
|
33
|
+
pos: { x: number; y: number };
|
|
34
|
+
setPos: (p: { x: number; y: number }) => void;
|
|
35
|
+
setLabel: (v: string | null) => void;
|
|
36
|
+
}) {
|
|
37
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
const onOver = (e: MouseEvent) => {
|
|
40
|
+
const t = (e.target as HTMLElement)?.closest<HTMLElement>("[data-tooltip]");
|
|
41
|
+
if (t) {
|
|
42
|
+
const r = t.getBoundingClientRect();
|
|
43
|
+
setPos({ x: r.left + r.width / 2, y: r.top });
|
|
44
|
+
setLabel(t.dataset.tooltip ?? null);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const onOut = (e: MouseEvent) => {
|
|
48
|
+
const t = (e.target as HTMLElement)?.closest<HTMLElement>("[data-tooltip]");
|
|
49
|
+
if (t) setLabel(null);
|
|
50
|
+
};
|
|
51
|
+
document.addEventListener("mouseover", onOver);
|
|
52
|
+
document.addEventListener("mouseout", onOut);
|
|
53
|
+
return () => {
|
|
54
|
+
document.removeEventListener("mouseover", onOver);
|
|
55
|
+
document.removeEventListener("mouseout", onOut);
|
|
56
|
+
};
|
|
57
|
+
}, [setLabel, setPos, pos]);
|
|
58
|
+
void ref;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function Tooltip({
|
|
63
|
+
children,
|
|
64
|
+
label,
|
|
65
|
+
}: {
|
|
66
|
+
children: React.ReactElement;
|
|
67
|
+
label: string;
|
|
68
|
+
}) {
|
|
69
|
+
return React.cloneElement(children, { "data-tooltip": label });
|
|
70
|
+
}
|