@swift-rust/ui 0.2.0 → 0.6.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 +66 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +89 -41
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.d.ts +2 -0
- package/dist/cli.test.d.ts.map +1 -0
- package/dist/cli.test.js +36 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +61 -32
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/registry.test.d.ts +2 -0
- package/dist/registry.test.d.ts.map +1 -0
- package/dist/registry.test.js +82 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/smoke.test.js +5 -3
- package/dist/smoke.test.js.map +1 -1
- package/package.json +7 -7
- package/registry/components/accordion.tsx +125 -16
- package/registry/components/alert-dialog.tsx +102 -0
- package/registry/components/alert.tsx +114 -14
- package/registry/components/aspect-ratio.tsx +18 -0
- package/registry/components/avatar.tsx +59 -7
- package/registry/components/badge.tsx +29 -14
- package/registry/components/breadcrumb.tsx +7 -13
- package/registry/components/button-group.tsx +28 -0
- package/registry/components/button.tsx +113 -28
- package/registry/components/calendar.tsx +92 -0
- package/registry/components/callout.tsx +14 -14
- package/registry/components/card.tsx +87 -12
- package/registry/components/carousel.tsx +41 -0
- package/registry/components/chart.tsx +50 -0
- package/registry/components/checkbox.tsx +5 -5
- package/registry/components/code-block.tsx +118 -0
- package/registry/components/code.tsx +2 -3
- package/registry/components/collapsible.tsx +60 -0
- package/registry/components/combobox.tsx +102 -0
- package/registry/components/command.tsx +5 -5
- package/registry/components/context-menu.tsx +81 -0
- package/registry/components/data-table.tsx +71 -0
- package/registry/components/date-picker.tsx +58 -0
- package/registry/components/dialog.tsx +2 -2
- package/registry/components/direction.tsx +17 -0
- package/registry/components/drawer.tsx +77 -0
- package/registry/components/dropdown-menu.tsx +5 -5
- package/registry/components/empty.tsx +34 -0
- package/registry/components/field.tsx +27 -0
- package/registry/components/file-upload.tsx +116 -0
- package/registry/components/form.tsx +3 -4
- package/registry/components/hover-card.tsx +59 -0
- package/registry/components/input-group.tsx +34 -0
- package/registry/components/input-otp.tsx +50 -0
- package/registry/components/input.tsx +71 -7
- package/registry/components/item.tsx +42 -0
- package/registry/components/kbd.tsx +3 -4
- package/registry/components/label.tsx +34 -4
- package/registry/components/menubar.tsx +60 -0
- package/registry/components/native-select.tsx +35 -0
- package/registry/components/navigation-menu.tsx +3 -3
- package/registry/components/pagination.tsx +4 -5
- package/registry/components/popover.tsx +1 -1
- package/registry/components/progress.tsx +10 -5
- package/registry/components/radio-group.tsx +9 -9
- package/registry/components/resizable.tsx +77 -0
- package/registry/components/scroll-area.tsx +20 -0
- package/registry/components/select.tsx +2 -3
- package/registry/components/separator.tsx +1 -2
- package/registry/components/sheet.tsx +1 -1
- package/registry/components/sidebar.tsx +72 -0
- package/registry/components/skeleton.tsx +1 -6
- package/registry/components/slider.tsx +6 -3
- package/registry/components/sonner.tsx +52 -0
- package/registry/components/spinner.tsx +19 -6
- package/registry/components/stepper.tsx +63 -0
- package/registry/components/switch.tsx +7 -6
- package/registry/components/table.tsx +2 -3
- package/registry/components/tabs.tsx +3 -3
- package/registry/components/textarea.tsx +42 -6
- package/registry/components/toast.tsx +2 -2
- package/registry/components/toggle-group.tsx +72 -0
- package/registry/components/toggle.tsx +45 -20
- package/registry/components/tooltip.tsx +4 -2
|
@@ -61,7 +61,7 @@ export function DialogContent({
|
|
|
61
61
|
role="dialog"
|
|
62
62
|
aria-modal
|
|
63
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-
|
|
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-border bg-background p-6 shadow-lg rounded-xl",
|
|
65
65
|
className,
|
|
66
66
|
)}
|
|
67
67
|
{...props}
|
|
@@ -84,7 +84,7 @@ export function DialogTitle({ className, ...props }: React.HTMLAttributes<HTMLHe
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export function DialogDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
87
|
-
return <p className={cn("text-sm text-
|
|
87
|
+
return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function DialogTrigger({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type Direction = "ltr" | "rtl";
|
|
4
|
+
|
|
5
|
+
const DirectionContext = React.createContext<Direction>("ltr");
|
|
6
|
+
|
|
7
|
+
export function DirectionProvider({ dir, children }: { dir: Direction; children: React.ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<DirectionContext.Provider value={dir}>
|
|
10
|
+
<div dir={dir}>{children}</div>
|
|
11
|
+
</DirectionContext.Provider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useDirection(): Direction {
|
|
16
|
+
return React.useContext(DirectionContext);
|
|
17
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const DrawerContext = React.createContext<{ open: boolean; setOpen: (v: boolean) => void } | null>(null);
|
|
6
|
+
|
|
7
|
+
export function Drawer({
|
|
8
|
+
open: controlled,
|
|
9
|
+
onOpenChange,
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
open?: boolean;
|
|
13
|
+
onOpenChange?: (v: boolean) => void;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}) {
|
|
16
|
+
const [internal, setInternal] = React.useState(false);
|
|
17
|
+
const open = controlled ?? internal;
|
|
18
|
+
const setOpen = (v: boolean) => {
|
|
19
|
+
if (controlled === undefined) setInternal(v);
|
|
20
|
+
onOpenChange?.(v);
|
|
21
|
+
};
|
|
22
|
+
return <DrawerContext.Provider value={{ open, setOpen }}>{children}</DrawerContext.Provider>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function DrawerTrigger({ asChild, children }: { asChild?: boolean; children: React.ReactNode }) {
|
|
26
|
+
const ctx = React.useContext(DrawerContext);
|
|
27
|
+
if (asChild && React.isValidElement(children)) {
|
|
28
|
+
return React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, {
|
|
29
|
+
onClick: () => ctx?.setOpen(true),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return <button type="button" onClick={() => ctx?.setOpen(true)}>{children}</button>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function DrawerContent({ className, children }: { className?: string; children: React.ReactNode }) {
|
|
36
|
+
const ctx = React.useContext(DrawerContext);
|
|
37
|
+
if (!ctx?.open) return null;
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
<div className="fixed inset-0 z-50 bg-black/60" onClick={() => ctx.setOpen(false)} aria-hidden />
|
|
41
|
+
<div
|
|
42
|
+
role="dialog"
|
|
43
|
+
aria-modal
|
|
44
|
+
className={cn(
|
|
45
|
+
"fixed inset-x-0 bottom-0 z-50 mx-auto flex max-h-[85vh] w-full max-w-lg flex-col gap-4 rounded-t-2xl border border-border bg-background p-6 shadow-lg",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<div className="mx-auto h-1.5 w-12 rounded-full bg-muted" />
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DrawerClose({ asChild, children }: { asChild?: boolean; children: React.ReactNode }) {
|
|
57
|
+
const ctx = React.useContext(DrawerContext);
|
|
58
|
+
if (asChild && React.isValidElement(children)) {
|
|
59
|
+
return React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, {
|
|
60
|
+
onClick: () => ctx?.setOpen(false),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return <button type="button" onClick={() => ctx?.setOpen(false)}>{children}</button>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function DrawerHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
67
|
+
return <div className={cn("flex flex-col gap-1.5 text-center sm:text-left", className)} {...props} />;
|
|
68
|
+
}
|
|
69
|
+
export function DrawerFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
70
|
+
return <div className={cn("mt-auto flex flex-col gap-2", className)} {...props} />;
|
|
71
|
+
}
|
|
72
|
+
export function DrawerTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
73
|
+
return <h2 className={cn("text-lg font-semibold", className)} {...props} />;
|
|
74
|
+
}
|
|
75
|
+
export function DrawerDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
76
|
+
return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
|
|
77
|
+
}
|
|
@@ -59,7 +59,7 @@ export function DropdownMenuContent({
|
|
|
59
59
|
<div
|
|
60
60
|
role="menu"
|
|
61
61
|
className={cn(
|
|
62
|
-
"absolute z-50 mt-1 min-w-[10rem] overflow-hidden rounded-md border border-
|
|
62
|
+
"absolute z-50 mt-1 min-w-[10rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-foreground shadow-md",
|
|
63
63
|
align === "end" && "right-0",
|
|
64
64
|
align === "center" && "left-1/2 -translate-x-1/2",
|
|
65
65
|
className,
|
|
@@ -78,8 +78,8 @@ export function DropdownMenuItem({
|
|
|
78
78
|
<div
|
|
79
79
|
role="menuitem"
|
|
80
80
|
className={cn(
|
|
81
|
-
"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-
|
|
82
|
-
"hover:bg-
|
|
81
|
+
"relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors",
|
|
82
|
+
"hover:bg-muted focus:bg-muted",
|
|
83
83
|
className,
|
|
84
84
|
)}
|
|
85
85
|
{...props}
|
|
@@ -88,9 +88,9 @@ export function DropdownMenuItem({
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function DropdownMenuSeparator() {
|
|
91
|
-
return <div className="my-1 h-px bg-
|
|
91
|
+
return <div className="my-1 h-px bg-border" />;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export function DropdownMenuLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
95
|
-
return <div className={cn("px-2 py-1.5 text-xs font-semibold text-
|
|
95
|
+
return <div className={cn("px-2 py-1.5 text-xs font-semibold text-muted-foreground", className)} {...props} />;
|
|
96
96
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export function Empty({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
className={cn(
|
|
8
|
+
"flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-border p-10 text-center",
|
|
9
|
+
className,
|
|
10
|
+
)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function EmptyMedia({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cn(
|
|
20
|
+
"flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground [&_svg]:size-6",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function EmptyTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
29
|
+
return <h3 className={cn("text-base font-semibold", className)} {...props} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function EmptyDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
33
|
+
return <p className={cn("max-w-sm text-sm text-muted-foreground", className)} {...props} />;
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export function Field({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
5
|
+
return <div className={cn("flex flex-col gap-1.5", className)} {...props} />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function FieldLabel({ className, ...props }: React.LabelHTMLAttributes<HTMLLabelElement>) {
|
|
9
|
+
return (
|
|
10
|
+
<label
|
|
11
|
+
className={cn("text-sm font-medium leading-none text-foreground", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function FieldDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
18
|
+
return <p className={cn("text-xs text-muted-foreground", className)} {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function FieldError({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
22
|
+
return <p className={cn("text-xs font-medium text-destructive", className)} {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function FieldGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
26
|
+
return <div className={cn("flex flex-col gap-4", className)} {...props} />;
|
|
27
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export interface UploadFile {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
size: number;
|
|
9
|
+
progress: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatSize(bytes: number) {
|
|
13
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
14
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
15
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FileUpload({
|
|
19
|
+
multiple = true,
|
|
20
|
+
accept,
|
|
21
|
+
onFiles,
|
|
22
|
+
className,
|
|
23
|
+
}: {
|
|
24
|
+
multiple?: boolean;
|
|
25
|
+
accept?: string;
|
|
26
|
+
onFiles?: (files: File[]) => void;
|
|
27
|
+
className?: string;
|
|
28
|
+
}) {
|
|
29
|
+
const [dragging, setDragging] = React.useState(false);
|
|
30
|
+
const [files, setFiles] = React.useState<UploadFile[]>([]);
|
|
31
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
32
|
+
const counter = React.useRef(0);
|
|
33
|
+
|
|
34
|
+
const add = (list: FileList | null) => {
|
|
35
|
+
if (!list) return;
|
|
36
|
+
const arr = Array.from(list);
|
|
37
|
+
onFiles?.(arr);
|
|
38
|
+
const mapped = arr.map((f) => ({ id: ++counter.current, name: f.name, size: f.size, progress: 0 }));
|
|
39
|
+
setFiles((prev) => (multiple ? [...prev, ...mapped] : mapped));
|
|
40
|
+
// simulate upload progress
|
|
41
|
+
for (const m of mapped) {
|
|
42
|
+
let p = 0;
|
|
43
|
+
const tick = setInterval(() => {
|
|
44
|
+
p += Math.random() * 25 + 10;
|
|
45
|
+
setFiles((prev) => prev.map((x) => (x.id === m.id ? { ...x, progress: Math.min(100, p) } : x)));
|
|
46
|
+
if (p >= 100) clearInterval(tick);
|
|
47
|
+
}, 220);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className={cn("flex w-full max-w-md flex-col gap-3", className)}>
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => inputRef.current?.click()}
|
|
56
|
+
onDragOver={(e) => {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
setDragging(true);
|
|
59
|
+
}}
|
|
60
|
+
onDragLeave={() => setDragging(false)}
|
|
61
|
+
onDrop={(e) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
setDragging(false);
|
|
64
|
+
add(e.dataTransfer.files);
|
|
65
|
+
}}
|
|
66
|
+
className={cn(
|
|
67
|
+
"flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed p-8 text-center transition-colors",
|
|
68
|
+
dragging ? "border-primary bg-primary/5" : "border-border hover:border-border-strong hover:bg-muted/50",
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<svg viewBox="0 0 24 24" className="size-7 text-muted-foreground" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden>
|
|
72
|
+
<path d="M12 16V4M7 9l5-5 5 5" strokeLinecap="round" strokeLinejoin="round" />
|
|
73
|
+
<path d="M5 16v2a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-2" strokeLinecap="round" />
|
|
74
|
+
</svg>
|
|
75
|
+
<span className="text-sm font-medium">Drag & drop or click to upload</span>
|
|
76
|
+
<span className="text-xs text-muted-foreground">{accept ?? "Any file"}{multiple ? " · multiple" : ""}</span>
|
|
77
|
+
</button>
|
|
78
|
+
<input
|
|
79
|
+
ref={inputRef}
|
|
80
|
+
type="file"
|
|
81
|
+
multiple={multiple}
|
|
82
|
+
accept={accept}
|
|
83
|
+
className="hidden"
|
|
84
|
+
onChange={(e) => add(e.target.files)}
|
|
85
|
+
/>
|
|
86
|
+
{files.length > 0 && (
|
|
87
|
+
<ul className="flex flex-col gap-2">
|
|
88
|
+
{files.map((f) => (
|
|
89
|
+
<li key={f.id} className="flex items-center gap-3 rounded-lg border border-border bg-card p-2.5">
|
|
90
|
+
<div className="flex size-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
|
91
|
+
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6" /></svg>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="min-w-0 flex-1">
|
|
94
|
+
<div className="flex items-center justify-between gap-2">
|
|
95
|
+
<span className="truncate text-sm font-medium">{f.name}</span>
|
|
96
|
+
<span className="shrink-0 text-xs text-muted-foreground">{formatSize(f.size)}</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-secondary">
|
|
99
|
+
<div className="h-full rounded-full bg-primary transition-all" style={{ width: `${f.progress}%` }} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onClick={() => setFiles((prev) => prev.filter((x) => x.id !== f.id))}
|
|
105
|
+
aria-label="Remove"
|
|
106
|
+
className="inline-flex size-7 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
107
|
+
>
|
|
108
|
+
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden><path d="M18 6 6 18M6 6l12 12" strokeLinecap="round" /></svg>
|
|
109
|
+
</button>
|
|
110
|
+
</li>
|
|
111
|
+
))}
|
|
112
|
+
</ul>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use client";
|
|
2
1
|
import * as React from "react";
|
|
3
2
|
import { cn } from "@/lib/utils";
|
|
4
3
|
|
|
@@ -19,10 +18,10 @@ export function FormField({
|
|
|
19
18
|
}) {
|
|
20
19
|
return (
|
|
21
20
|
<div className="space-y-1.5">
|
|
22
|
-
{label ? <label className="text-sm font-medium text-
|
|
21
|
+
{label ? <label className="text-sm font-medium text-foreground">{label}</label> : null}
|
|
23
22
|
{children}
|
|
24
|
-
{description && !error ? <p className="text-xs text-
|
|
25
|
-
{error ? <p className="text-xs text-
|
|
23
|
+
{description && !error ? <p className="text-xs text-muted-foreground">{description}</p> : null}
|
|
24
|
+
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
|
26
25
|
</div>
|
|
27
26
|
);
|
|
28
27
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
const HoverCardContext = React.createContext<{
|
|
6
|
+
open: boolean;
|
|
7
|
+
show: () => void;
|
|
8
|
+
hide: () => void;
|
|
9
|
+
} | null>(null);
|
|
10
|
+
|
|
11
|
+
export function HoverCard({ children, openDelay = 200 }: { children: React.ReactNode; openDelay?: number }) {
|
|
12
|
+
const [open, setOpen] = React.useState(false);
|
|
13
|
+
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
14
|
+
const show = () => {
|
|
15
|
+
if (timer.current) clearTimeout(timer.current);
|
|
16
|
+
timer.current = setTimeout(() => setOpen(true), openDelay);
|
|
17
|
+
};
|
|
18
|
+
const hide = () => {
|
|
19
|
+
if (timer.current) clearTimeout(timer.current);
|
|
20
|
+
setOpen(false);
|
|
21
|
+
};
|
|
22
|
+
return (
|
|
23
|
+
<HoverCardContext.Provider value={{ open, show, hide }}>
|
|
24
|
+
<span className="relative inline-block" onMouseEnter={show} onMouseLeave={hide}>
|
|
25
|
+
{children}
|
|
26
|
+
</span>
|
|
27
|
+
</HoverCardContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function HoverCardTrigger({ asChild, children }: { asChild?: boolean; children: React.ReactNode }) {
|
|
32
|
+
if (asChild) return <>{children}</>;
|
|
33
|
+
return <span className="cursor-default">{children}</span>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function HoverCardContent({
|
|
37
|
+
className,
|
|
38
|
+
children,
|
|
39
|
+
align = "center",
|
|
40
|
+
}: {
|
|
41
|
+
className?: string;
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
align?: "start" | "center" | "end";
|
|
44
|
+
}) {
|
|
45
|
+
const ctx = React.useContext(HoverCardContext);
|
|
46
|
+
if (!ctx?.open) return null;
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
"absolute top-full z-50 mt-2 w-64 rounded-lg border border-border bg-popover p-4 text-sm text-popover-foreground shadow-md",
|
|
51
|
+
align === "center" && "left-1/2 -translate-x-1/2",
|
|
52
|
+
align === "end" && "right-0",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
// A row that fuses addons (icons, text, buttons) with an input into one control.
|
|
5
|
+
export function InputGroup({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
className={cn(
|
|
9
|
+
"flex h-9 w-full items-center rounded-lg border border-input bg-background text-sm shadow-xs transition-[color,box-shadow]",
|
|
10
|
+
"focus-within:ring-2 focus-within:ring-ring/50",
|
|
11
|
+
"[&>input]:h-full [&>input]:min-w-0 [&>input]:flex-1 [&>input]:border-0 [&>input]:bg-transparent [&>input]:px-3 [&>input]:outline-hidden",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function InputGroupAddon({
|
|
20
|
+
className,
|
|
21
|
+
align = "start",
|
|
22
|
+
...props
|
|
23
|
+
}: React.HTMLAttributes<HTMLDivElement> & { align?: "start" | "end" }) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex items-center gap-1.5 px-2.5 text-muted-foreground [&_svg]:size-4",
|
|
28
|
+
align === "end" ? "order-last" : "",
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
export function InputOTP({
|
|
6
|
+
length = 6,
|
|
7
|
+
value: controlled,
|
|
8
|
+
defaultValue = "",
|
|
9
|
+
onChange,
|
|
10
|
+
className,
|
|
11
|
+
}: {
|
|
12
|
+
length?: number;
|
|
13
|
+
value?: string;
|
|
14
|
+
defaultValue?: string;
|
|
15
|
+
onChange?: (v: string) => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}) {
|
|
18
|
+
const [internal, setInternal] = React.useState(defaultValue);
|
|
19
|
+
const value = controlled ?? internal;
|
|
20
|
+
const refs = React.useRef<Array<HTMLInputElement | null>>([]);
|
|
21
|
+
|
|
22
|
+
const setChar = (i: number, char: string) => {
|
|
23
|
+
const next = (value.slice(0, i) + char + value.slice(i + 1)).slice(0, length);
|
|
24
|
+
if (controlled === undefined) setInternal(next);
|
|
25
|
+
onChange?.(next);
|
|
26
|
+
if (char && i < length - 1) refs.current[i + 1]?.focus();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={cn("flex items-center gap-2", className)}>
|
|
31
|
+
{Array.from({ length }).map((_, i) => (
|
|
32
|
+
<input
|
|
33
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
34
|
+
key={i}
|
|
35
|
+
ref={(el) => {
|
|
36
|
+
refs.current[i] = el;
|
|
37
|
+
}}
|
|
38
|
+
inputMode="numeric"
|
|
39
|
+
maxLength={1}
|
|
40
|
+
value={value[i] ?? ""}
|
|
41
|
+
onChange={(e) => setChar(i, e.target.value.replace(/\D/g, "").slice(-1))}
|
|
42
|
+
onKeyDown={(e) => {
|
|
43
|
+
if (e.key === "Backspace" && !value[i] && i > 0) refs.current[i - 1]?.focus();
|
|
44
|
+
}}
|
|
45
|
+
className="size-10 rounded-lg border border-input bg-background text-center text-lg font-medium shadow-xs focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -1,19 +1,83 @@
|
|
|
1
|
-
"use client";
|
|
2
1
|
import * as React from "react";
|
|
3
2
|
import { cn } from "@/lib/utils";
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* swift-rust ui · Input
|
|
6
|
+
*
|
|
7
|
+
* variant — default, outline, secondary, ghost, destructive
|
|
8
|
+
* size — xs, sm, default, md, lg (the native `size` attribute is dropped)
|
|
9
|
+
* design — flat, soft, 3d, glass, neo, brutal, gradient
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type InputVariant = "default" | "outline" | "secondary" | "ghost" | "destructive";
|
|
13
|
+
export type InputSize = "default" | "xs" | "sm" | "md" | "lg";
|
|
14
|
+
export type InputDesign = "flat" | "soft" | "3d" | "glass" | "neo" | "brutal" | "gradient";
|
|
15
|
+
|
|
16
|
+
const VARIANTS: Record<InputVariant, string> = {
|
|
17
|
+
default: "border border-input bg-background",
|
|
18
|
+
outline: "border-2 border-input bg-transparent",
|
|
19
|
+
secondary: "border border-transparent bg-secondary",
|
|
20
|
+
ghost: "border border-transparent bg-transparent hover:bg-secondary/50",
|
|
21
|
+
destructive:
|
|
22
|
+
"border border-destructive text-destructive placeholder:text-destructive/60 focus-visible:ring-destructive/40",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SIZES: Record<InputSize, string> = {
|
|
26
|
+
default: "h-9 px-3 py-1 text-sm",
|
|
27
|
+
xs: "h-7 rounded-md px-2 text-xs",
|
|
28
|
+
sm: "h-8 px-2.5 text-xs",
|
|
29
|
+
md: "h-9 px-3 py-1 text-sm",
|
|
30
|
+
lg: "h-10 px-4 text-base",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const DESIGNS: Record<InputDesign, string> = {
|
|
34
|
+
flat: "shadow-xs",
|
|
35
|
+
soft: "rounded-xl border-transparent bg-secondary shadow-none",
|
|
36
|
+
// A field reads "deep" when it looks recessed: a darker top edge + a top→down
|
|
37
|
+
// shading gradient sink the surface in (no inset shadow).
|
|
38
|
+
"3d": "border-t-2 border-t-black/15 bg-linear-to-b from-black/[0.06] to-transparent dark:border-t-black/40",
|
|
39
|
+
// Liquid glass field: blur + saturation, translucent sheen, bright rim.
|
|
40
|
+
glass:
|
|
41
|
+
"border-white/40 bg-white/15 backdrop-blur-xl backdrop-saturate-200 placeholder:text-foreground/50 " +
|
|
42
|
+
"bg-linear-to-br from-white/30 to-white/5 " +
|
|
43
|
+
"dark:border-white/20 dark:bg-white/10 dark:from-white/15 dark:to-transparent",
|
|
44
|
+
neo:
|
|
45
|
+
"border-transparent bg-background " +
|
|
46
|
+
"shadow-[inset_3px_3px_6px_rgba(0,0,0,0.12),inset_-3px_-3px_6px_rgba(255,255,255,0.7)] " +
|
|
47
|
+
"dark:shadow-[inset_3px_3px_6px_rgba(0,0,0,0.6),inset_-3px_-3px_6px_rgba(255,255,255,0.05)]",
|
|
48
|
+
brutal:
|
|
49
|
+
"rounded-none border-2 border-foreground shadow-[3px_3px_0_0_var(--color-foreground)] " +
|
|
50
|
+
"focus-visible:shadow-[1px_1px_0_0_var(--color-foreground)] focus-visible:ring-0",
|
|
51
|
+
gradient:
|
|
52
|
+
"border-2 border-transparent " +
|
|
53
|
+
"[background:linear-gradient(var(--color-background),var(--color-background))_padding-box," +
|
|
54
|
+
"linear-gradient(135deg,#8b5cf6,#d946ef,#fb923c)_border-box]",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export interface InputProps
|
|
58
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
59
|
+
variant?: InputVariant;
|
|
60
|
+
size?: InputSize;
|
|
61
|
+
design?: InputDesign;
|
|
62
|
+
}
|
|
6
63
|
|
|
7
64
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
8
|
-
(
|
|
65
|
+
(
|
|
66
|
+
{ className, type = "text", variant = "default", size = "default", design = "flat", ...props },
|
|
67
|
+
ref,
|
|
68
|
+
) => (
|
|
9
69
|
<input
|
|
10
70
|
type={type}
|
|
11
71
|
ref={ref}
|
|
12
72
|
className={cn(
|
|
13
|
-
"flex
|
|
14
|
-
"placeholder:text-
|
|
15
|
-
"focus-visible:outline-
|
|
16
|
-
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
73
|
+
"flex w-full min-w-0 rounded-lg text-foreground transition-[color,box-shadow,background-color]",
|
|
74
|
+
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground",
|
|
75
|
+
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
|
|
76
|
+
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
77
|
+
"file:inline-flex file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground",
|
|
78
|
+
VARIANTS[variant],
|
|
79
|
+
SIZES[size],
|
|
80
|
+
DESIGNS[design],
|
|
17
81
|
className,
|
|
18
82
|
)}
|
|
19
83
|
{...props}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export function Item({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
className={cn(
|
|
8
|
+
"flex items-center gap-3 rounded-lg border border-border bg-card p-3 text-card-foreground",
|
|
9
|
+
className,
|
|
10
|
+
)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ItemMedia({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={cn(
|
|
20
|
+
"flex size-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground [&_svg]:size-4",
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ItemContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
29
|
+
return <div className={cn("flex min-w-0 flex-1 flex-col gap-0.5", className)} {...props} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ItemTitle({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
33
|
+
return <p className={cn("truncate text-sm font-medium leading-none", className)} {...props} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ItemDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
|
37
|
+
return <p className={cn("truncate text-sm text-muted-foreground", className)} {...props} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ItemActions({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
41
|
+
return <div className={cn("ml-auto flex shrink-0 items-center gap-1", className)} {...props} />;
|
|
42
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
"use client";
|
|
2
1
|
import * as React from "react";
|
|
3
2
|
import { cn } from "@/lib/utils";
|
|
4
3
|
|
|
5
|
-
export const Kbd = React.forwardRef<
|
|
4
|
+
export const Kbd = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
|
|
6
5
|
({ className, ...props }, ref) => (
|
|
7
|
-
<
|
|
6
|
+
<kbd
|
|
8
7
|
ref={ref}
|
|
9
8
|
className={cn(
|
|
10
|
-
"inline-flex h-5 select-none items-center gap-1 rounded border border-
|
|
9
|
+
"inline-flex h-5 select-none items-center gap-1 rounded border border-border bg-muted px-1.5 font-mono text-[0.6875rem] font-medium text-muted-foreground",
|
|
11
10
|
className,
|
|
12
11
|
)}
|
|
13
12
|
{...props}
|