@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.
Files changed (53) hide show
  1. package/dist/cli.d.ts +17 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +437 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/components.d.ts +8 -0
  6. package/dist/components.d.ts.map +1 -0
  7. package/dist/components.js +39 -0
  8. package/dist/components.js.map +1 -0
  9. package/dist/index.d.ts +29 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +34 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/smoke.test.d.ts +2 -0
  14. package/dist/smoke.test.d.ts.map +1 -0
  15. package/dist/smoke.test.js +18 -0
  16. package/dist/smoke.test.js.map +1 -0
  17. package/package.json +54 -0
  18. package/registry/components/accordion.tsx +106 -0
  19. package/registry/components/alert.tsx +34 -0
  20. package/registry/components/avatar.tsx +37 -0
  21. package/registry/components/badge.tsx +30 -0
  22. package/registry/components/breadcrumb.tsx +56 -0
  23. package/registry/components/button.tsx +48 -0
  24. package/registry/components/callout.tsx +20 -0
  25. package/registry/components/card.tsx +49 -0
  26. package/registry/components/checkbox.tsx +21 -0
  27. package/registry/components/code.tsx +27 -0
  28. package/registry/components/command.tsx +93 -0
  29. package/registry/components/dialog.tsx +118 -0
  30. package/registry/components/dropdown-menu.tsx +96 -0
  31. package/registry/components/form.tsx +28 -0
  32. package/registry/components/input.tsx +23 -0
  33. package/registry/components/kbd.tsx +17 -0
  34. package/registry/components/label.tsx +19 -0
  35. package/registry/components/navigation-menu.tsx +74 -0
  36. package/registry/components/pagination.tsx +56 -0
  37. package/registry/components/popover.tsx +58 -0
  38. package/registry/components/progress.tsx +22 -0
  39. package/registry/components/radio-group.tsx +78 -0
  40. package/registry/components/select.tsx +21 -0
  41. package/registry/components/separator.tsx +23 -0
  42. package/registry/components/sheet.tsx +68 -0
  43. package/registry/components/skeleton.tsx +14 -0
  44. package/registry/components/slider.tsx +18 -0
  45. package/registry/components/spinner.tsx +17 -0
  46. package/registry/components/switch.tsx +23 -0
  47. package/registry/components/table.tsx +65 -0
  48. package/registry/components/tabs.tsx +86 -0
  49. package/registry/components/textarea.tsx +22 -0
  50. package/registry/components/toast.tsx +58 -0
  51. package/registry/components/toggle.tsx +46 -0
  52. package/registry/components/tooltip.tsx +70 -0
  53. 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";