@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,118 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const DialogContext = React.createContext<{
|
|
6
|
+
open: boolean;
|
|
7
|
+
setOpen: (v: boolean) => void;
|
|
8
|
+
} | null>(null);
|
|
9
|
+
|
|
10
|
+
export function Dialog({
|
|
11
|
+
open: controlled,
|
|
12
|
+
onOpenChange,
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
open?: boolean;
|
|
16
|
+
onOpenChange?: (v: boolean) => void;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}) {
|
|
19
|
+
const [internal, setInternal] = React.useState(false);
|
|
20
|
+
const open = controlled ?? internal;
|
|
21
|
+
const setOpen = (v: boolean) => {
|
|
22
|
+
if (controlled === undefined) setInternal(v);
|
|
23
|
+
onOpenChange?.(v);
|
|
24
|
+
};
|
|
25
|
+
return (
|
|
26
|
+
<DialogContext.Provider value={{ open, setOpen }}>
|
|
27
|
+
{children}
|
|
28
|
+
{open ? <DialogOverlay /> : null}
|
|
29
|
+
</DialogContext.Provider>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function DialogOverlay() {
|
|
34
|
+
const ctx = React.useContext(DialogContext)!;
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm"
|
|
38
|
+
onClick={() => ctx.setOpen(false)}
|
|
39
|
+
aria-hidden
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function DialogContent({
|
|
45
|
+
className,
|
|
46
|
+
children,
|
|
47
|
+
...props
|
|
48
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
49
|
+
const ctx = React.useContext(DialogContext);
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (!ctx?.open) return;
|
|
52
|
+
const onKey = (e: KeyboardEvent) => {
|
|
53
|
+
if (e.key === "Escape") ctx.setOpen(false);
|
|
54
|
+
};
|
|
55
|
+
document.addEventListener("keydown", onKey);
|
|
56
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
57
|
+
}, [ctx]);
|
|
58
|
+
if (!ctx?.open) return null;
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
role="dialog"
|
|
62
|
+
aria-modal
|
|
63
|
+
className={cn(
|
|
64
|
+
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border border-[var(--ui-border)] bg-[var(--ui-surface)] p-6 shadow-lg rounded-xl",
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
75
|
+
return <div className={cn("flex flex-col space-y-1.5 text-left", className)} {...props} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
79
|
+
return <div className={cn("flex justify-end gap-2 pt-2", className)} {...props} />;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function DialogTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
83
|
+
return <h2 className={cn("text-lg font-semibold leading-none", className)} {...props} />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function DialogDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
87
|
+
return <p className={cn("text-sm text-[var(--ui-fg-muted)]", className)} {...props} />;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function DialogTrigger({
|
|
91
|
+
asChild,
|
|
92
|
+
children,
|
|
93
|
+
...props
|
|
94
|
+
}: React.HTMLAttributes<HTMLDivElement> & { asChild?: boolean }) {
|
|
95
|
+
const ctx = React.useContext(DialogContext)!;
|
|
96
|
+
if (asChild && React.isValidElement(children)) {
|
|
97
|
+
return React.cloneElement(children as React.ReactElement<{ onClick?: React.MouseEventHandler }>, {
|
|
98
|
+
onClick: (e: React.MouseEvent) => {
|
|
99
|
+
(children as React.ReactElement<{ onClick?: React.MouseEventHandler }>).props.onClick?.(e);
|
|
100
|
+
ctx.setOpen(true);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return (
|
|
105
|
+
<div {...props} onClick={() => ctx.setOpen(true)}>
|
|
106
|
+
{children}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function DialogClose({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
112
|
+
const ctx = React.useContext(DialogContext)!;
|
|
113
|
+
return (
|
|
114
|
+
<div {...props} onClick={() => ctx.setOpen(false)}>
|
|
115
|
+
{children}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const DropdownContext = React.createContext<{ open: boolean; setOpen: (v: boolean) => void } | null>(null);
|
|
6
|
+
|
|
7
|
+
export function DropdownMenu({
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}) {
|
|
12
|
+
const [open, setOpen] = React.useState(false);
|
|
13
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
if (!open) return;
|
|
16
|
+
const onClick = (e: MouseEvent) => {
|
|
17
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
18
|
+
};
|
|
19
|
+
document.addEventListener("mousedown", onClick);
|
|
20
|
+
return () => document.removeEventListener("mousedown", onClick);
|
|
21
|
+
}, [open]);
|
|
22
|
+
return (
|
|
23
|
+
<DropdownContext.Provider value={{ open, setOpen }}>
|
|
24
|
+
<div ref={ref} className="relative inline-block">
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
</DropdownContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DropdownMenuTrigger({
|
|
32
|
+
asChild,
|
|
33
|
+
children,
|
|
34
|
+
}: {
|
|
35
|
+
asChild?: boolean;
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
}) {
|
|
38
|
+
const ctx = React.useContext(DropdownContext)!;
|
|
39
|
+
if (asChild && React.isValidElement(children)) {
|
|
40
|
+
return React.cloneElement(children as React.ReactElement<{ onClick?: React.MouseEventHandler }>, {
|
|
41
|
+
onClick: () => ctx.setOpen(!ctx.open),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return <button type="button" onClick={() => ctx.setOpen(!ctx.open)}>{children}</button>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function DropdownMenuContent({
|
|
48
|
+
align = "start",
|
|
49
|
+
className,
|
|
50
|
+
children,
|
|
51
|
+
}: {
|
|
52
|
+
align?: "start" | "end" | "center";
|
|
53
|
+
className?: string;
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
}) {
|
|
56
|
+
const ctx = React.useContext(DropdownContext)!;
|
|
57
|
+
if (!ctx.open) return null;
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
role="menu"
|
|
61
|
+
className={cn(
|
|
62
|
+
"absolute z-50 mt-1 min-w-[10rem] overflow-hidden rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] p-1 text-[var(--ui-fg)] shadow-md",
|
|
63
|
+
align === "end" && "right-0",
|
|
64
|
+
align === "center" && "left-1/2 -translate-x-1/2",
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function DropdownMenuItem({
|
|
74
|
+
className,
|
|
75
|
+
...props
|
|
76
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
role="menuitem"
|
|
80
|
+
className={cn(
|
|
81
|
+
"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors",
|
|
82
|
+
"hover:bg-[var(--ui-surface-2)] focus:bg-[var(--ui-surface-2)]",
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function DropdownMenuSeparator() {
|
|
91
|
+
return <div className="my-1 h-px bg-[var(--ui-border)]" />;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function DropdownMenuLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
95
|
+
return <div className={cn("px-2 py-1.5 text-xs font-semibold text-[var(--ui-fg-subtle)]", className)} {...props} />;
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export function Form({ className, ...props }: React.FormHTMLAttributes<HTMLFormElement>) {
|
|
6
|
+
return <form className={cn("space-y-4", className)} {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function FormField({
|
|
10
|
+
label,
|
|
11
|
+
description,
|
|
12
|
+
error,
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
label?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="space-y-1.5">
|
|
22
|
+
{label ? <label className="text-sm font-medium text-[var(--ui-fg)]">{label}</label> : null}
|
|
23
|
+
{children}
|
|
24
|
+
{description && !error ? <p className="text-xs text-[var(--ui-fg-muted)]">{description}</p> : null}
|
|
25
|
+
{error ? <p className="text-xs text-[var(--ui-danger)]">{error}</p> : null}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
|
6
|
+
|
|
7
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
8
|
+
({ className, type = "text", ...props }, ref) => (
|
|
9
|
+
<input
|
|
10
|
+
type={type}
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex h-9 w-full rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] px-3 py-1 text-sm shadow-sm",
|
|
14
|
+
"placeholder:text-[var(--ui-fg-subtle)]",
|
|
15
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-accent)] focus-visible:ring-offset-1",
|
|
16
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
Input.displayName = "Input";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Kbd = React.forwardRef<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
|
|
6
|
+
({ className, ...props }, ref) => (
|
|
7
|
+
<span
|
|
8
|
+
ref={ref}
|
|
9
|
+
className={cn(
|
|
10
|
+
"inline-flex h-5 select-none items-center gap-1 rounded border border-[var(--ui-border-strong)] bg-[var(--ui-surface-2)] px-1.5 font-mono text-[0.6875rem] font-medium",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
Kbd.displayName = "Kbd";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
|
|
6
|
+
|
|
7
|
+
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
8
|
+
({ className, ...props }, ref) => (
|
|
9
|
+
<label
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"text-sm font-medium leading-none text-[var(--ui-fg)] peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
Label.displayName = "Label";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const NavigationMenuContext = React.createContext<{ openValue: string | null; setOpenValue: (v: string | null) => void } | null>(null);
|
|
6
|
+
|
|
7
|
+
export function NavigationMenu({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
8
|
+
const [openValue, setOpenValue] = React.useState<string | null>(null);
|
|
9
|
+
return (
|
|
10
|
+
<NavigationMenuContext.Provider value={{ openValue, setOpenValue }}>
|
|
11
|
+
<nav className={cn("relative", className)}>{children}</nav>
|
|
12
|
+
</NavigationMenuContext.Provider>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function NavigationMenuList({ className, children }: { className?: string; children: React.ReactNode }) {
|
|
17
|
+
return <ul className={cn("flex items-center gap-1", className)}>{children}</ul>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function NavigationMenuItem({ value, children }: { value: string; children: React.ReactNode }) {
|
|
21
|
+
const ctx = React.useContext(NavigationMenuContext)!;
|
|
22
|
+
return (
|
|
23
|
+
<li
|
|
24
|
+
onMouseEnter={() => ctx.setOpenValue(value)}
|
|
25
|
+
onMouseLeave={() => ctx.setOpenValue(null)}
|
|
26
|
+
className="relative"
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</li>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function NavigationMenuTrigger({ children }: { children: React.ReactNode }) {
|
|
34
|
+
return (
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
className="inline-flex h-9 items-center gap-1 rounded-md px-3 py-1 text-sm hover:bg-[var(--ui-surface-2)]"
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
<svg viewBox="0 0 24 24" className="h-3 w-3" fill="none" stroke="currentColor" strokeWidth="2">
|
|
41
|
+
<path d="M6 9l6 6 6-6" strokeLinecap="round" />
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function NavigationMenuContent({ value, className, children }: { value: string; className?: string; children: React.ReactNode }) {
|
|
48
|
+
const ctx = React.useContext(NavigationMenuContext)!;
|
|
49
|
+
if (ctx.openValue !== value) return null;
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
"absolute left-0 top-full mt-1 min-w-[200px] rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] p-2 shadow-md",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function NavigationMenuLink({ href, children, className }: { href: string; children: React.ReactNode; className?: string }) {
|
|
63
|
+
return (
|
|
64
|
+
<a
|
|
65
|
+
href={href}
|
|
66
|
+
className={cn(
|
|
67
|
+
"block rounded-sm px-2 py-1.5 text-sm text-[var(--ui-fg-muted)] hover:bg-[var(--ui-surface-2)] hover:text-[var(--ui-fg)]",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</a>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface PaginationProps {
|
|
6
|
+
page: number;
|
|
7
|
+
total: number;
|
|
8
|
+
onChange: (page: number) => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Pagination({ page, total, onChange, className }: PaginationProps) {
|
|
13
|
+
const pages: Array<number | "ellipsis"> = [];
|
|
14
|
+
for (let i = 1; i <= total; i++) {
|
|
15
|
+
if (i === 1 || i === total || Math.abs(i - page) <= 1) pages.push(i);
|
|
16
|
+
else if (pages[pages.length - 1] !== "ellipsis") pages.push("ellipsis");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<nav className={cn("flex items-center gap-1", className)}>
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
onClick={() => onChange(Math.max(1, page - 1))}
|
|
24
|
+
disabled={page === 1}
|
|
25
|
+
className="inline-flex h-8 items-center gap-1 rounded-md px-2 text-sm hover:bg-[var(--ui-surface-2)] disabled:opacity-50"
|
|
26
|
+
>
|
|
27
|
+
←
|
|
28
|
+
</button>
|
|
29
|
+
{pages.map((p, i) =>
|
|
30
|
+
p === "ellipsis" ? (
|
|
31
|
+
<span key={`e${i}`} className="px-2 text-[var(--ui-fg-subtle)]">…</span>
|
|
32
|
+
) : (
|
|
33
|
+
<button
|
|
34
|
+
key={p}
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => onChange(p)}
|
|
37
|
+
className={cn(
|
|
38
|
+
"h-8 w-8 rounded-md text-sm transition-colors",
|
|
39
|
+
p === page ? "bg-[var(--ui-fg)] text-[var(--ui-bg)]" : "hover:bg-[var(--ui-surface-2)]",
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{p}
|
|
43
|
+
</button>
|
|
44
|
+
),
|
|
45
|
+
)}
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={() => onChange(Math.min(total, page + 1))}
|
|
49
|
+
disabled={page === total}
|
|
50
|
+
className="inline-flex h-8 items-center gap-1 rounded-md px-2 text-sm hover:bg-[var(--ui-surface-2)] disabled:opacity-50"
|
|
51
|
+
>
|
|
52
|
+
→
|
|
53
|
+
</button>
|
|
54
|
+
</nav>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const PopoverContext = React.createContext<{ open: boolean; setOpen: (v: boolean) => void } | null>(null);
|
|
6
|
+
|
|
7
|
+
export function Popover({ children }: { children: React.ReactNode }) {
|
|
8
|
+
const [open, setOpen] = React.useState(false);
|
|
9
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
if (!open) return;
|
|
12
|
+
const onClick = (e: MouseEvent) => {
|
|
13
|
+
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
|
|
14
|
+
};
|
|
15
|
+
document.addEventListener("mousedown", onClick);
|
|
16
|
+
return () => document.removeEventListener("mousedown", onClick);
|
|
17
|
+
}, [open]);
|
|
18
|
+
return (
|
|
19
|
+
<PopoverContext.Provider value={{ open, setOpen }}>
|
|
20
|
+
<div ref={ref} className="relative inline-block">{children}</div>
|
|
21
|
+
</PopoverContext.Provider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PopoverTrigger({ asChild, children }: { asChild?: boolean; children: React.ReactNode }) {
|
|
26
|
+
const ctx = React.useContext(PopoverContext)!;
|
|
27
|
+
if (asChild && React.isValidElement(children)) {
|
|
28
|
+
return React.cloneElement(children as React.ReactElement<{ onClick?: React.MouseEventHandler }>, {
|
|
29
|
+
onClick: () => ctx.setOpen(!ctx.open),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return <button type="button" onClick={() => ctx.setOpen(!ctx.open)}>{children}</button>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function PopoverContent({
|
|
36
|
+
className,
|
|
37
|
+
children,
|
|
38
|
+
align = "start",
|
|
39
|
+
}: {
|
|
40
|
+
className?: string;
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
align?: "start" | "end" | "center";
|
|
43
|
+
}) {
|
|
44
|
+
const ctx = React.useContext(PopoverContext)!;
|
|
45
|
+
if (!ctx.open) return null;
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={cn(
|
|
49
|
+
"absolute z-50 mt-1 w-72 rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] p-4 text-sm shadow-md",
|
|
50
|
+
align === "end" && "right-0",
|
|
51
|
+
align === "center" && "left-1/2 -translate-x-1/2",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Progress = React.forwardRef<HTMLDivElement, { value: number; max?: number; className?: string }>(
|
|
6
|
+
({ className, value, max = 100, ...props }, ref) => (
|
|
7
|
+
<div
|
|
8
|
+
ref={ref}
|
|
9
|
+
role="progressbar"
|
|
10
|
+
aria-valuenow={value}
|
|
11
|
+
aria-valuemax={max}
|
|
12
|
+
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-[var(--ui-surface-2)]", className)}
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<div
|
|
16
|
+
className="h-full bg-[var(--ui-accent)] transition-all"
|
|
17
|
+
style={{ width: `${Math.min(100, (value / max) * 100)}%` }}
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
Progress.displayName = "Progress";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const RadioGroupContext = React.createContext<{
|
|
6
|
+
value: string;
|
|
7
|
+
setValue: (v: string) => void;
|
|
8
|
+
name: string;
|
|
9
|
+
} | null>(null);
|
|
10
|
+
|
|
11
|
+
export function RadioGroup({
|
|
12
|
+
value: controlled,
|
|
13
|
+
onValueChange,
|
|
14
|
+
defaultValue,
|
|
15
|
+
name = "radio",
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
value?: string;
|
|
20
|
+
onValueChange?: (v: string) => void;
|
|
21
|
+
defaultValue?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
const [internal, setInternal] = React.useState(defaultValue ?? "");
|
|
27
|
+
const value = controlled ?? internal;
|
|
28
|
+
const setValue = (v: string) => {
|
|
29
|
+
if (controlled === undefined) setInternal(v);
|
|
30
|
+
onValueChange?.(v);
|
|
31
|
+
};
|
|
32
|
+
return (
|
|
33
|
+
<RadioGroupContext.Provider value={{ value, setValue, name }}>
|
|
34
|
+
<div role="radiogroup" className={cn("space-y-1", className)}>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
</RadioGroupContext.Provider>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function RadioGroupItem({
|
|
42
|
+
value,
|
|
43
|
+
label,
|
|
44
|
+
className,
|
|
45
|
+
}: {
|
|
46
|
+
value: string;
|
|
47
|
+
label?: React.ReactNode;
|
|
48
|
+
className?: string;
|
|
49
|
+
}) {
|
|
50
|
+
const ctx = React.useContext(RadioGroupContext)!;
|
|
51
|
+
const active = ctx.value === value;
|
|
52
|
+
return (
|
|
53
|
+
<label
|
|
54
|
+
className={cn(
|
|
55
|
+
"flex cursor-pointer items-center gap-2 rounded-md p-2 text-sm transition-colors hover:bg-[var(--ui-surface-2)]",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<span
|
|
60
|
+
className={cn(
|
|
61
|
+
"flex h-4 w-4 items-center justify-center rounded-full border",
|
|
62
|
+
active ? "border-[var(--ui-accent)]" : "border-[var(--ui-border-strong)]",
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{active ? <span className="h-2 w-2 rounded-full bg-[var(--ui-accent)]" /> : null}
|
|
66
|
+
</span>
|
|
67
|
+
<input
|
|
68
|
+
type="radio"
|
|
69
|
+
name={ctx.name}
|
|
70
|
+
value={value}
|
|
71
|
+
checked={active}
|
|
72
|
+
onChange={() => ctx.setValue(value)}
|
|
73
|
+
className="sr-only"
|
|
74
|
+
/>
|
|
75
|
+
{label}
|
|
76
|
+
</label>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export const Select = React.forwardRef<HTMLSelectElement, React.SelectHTMLAttributes<HTMLSelectElement>>(
|
|
6
|
+
({ className, children, ...props }, ref) => (
|
|
7
|
+
<select
|
|
8
|
+
ref={ref}
|
|
9
|
+
className={cn(
|
|
10
|
+
"flex h-9 w-full rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] px-3 py-1 text-sm shadow-sm",
|
|
11
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-accent)] focus-visible:ring-offset-1",
|
|
12
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</select>
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
Select.displayName = "Select";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export type SeparatorProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
6
|
+
orientation?: "horizontal" | "vertical";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
|
|
10
|
+
({ className, orientation = "horizontal", ...props }, ref) => (
|
|
11
|
+
<div
|
|
12
|
+
ref={ref}
|
|
13
|
+
role="separator"
|
|
14
|
+
className={cn(
|
|
15
|
+
"shrink-0 bg-[var(--ui-border)]",
|
|
16
|
+
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
Separator.displayName = "Separator";
|