@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.
Files changed (85) hide show
  1. package/README.md +66 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +89 -41
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cli.test.d.ts +2 -0
  6. package/dist/cli.test.d.ts.map +1 -0
  7. package/dist/cli.test.js +36 -0
  8. package/dist/cli.test.js.map +1 -0
  9. package/dist/components.d.ts.map +1 -1
  10. package/dist/components.js +61 -32
  11. package/dist/components.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/registry.test.d.ts +2 -0
  16. package/dist/registry.test.d.ts.map +1 -0
  17. package/dist/registry.test.js +82 -0
  18. package/dist/registry.test.js.map +1 -0
  19. package/dist/smoke.test.js +5 -3
  20. package/dist/smoke.test.js.map +1 -1
  21. package/package.json +7 -7
  22. package/registry/components/accordion.tsx +125 -16
  23. package/registry/components/alert-dialog.tsx +102 -0
  24. package/registry/components/alert.tsx +114 -14
  25. package/registry/components/aspect-ratio.tsx +18 -0
  26. package/registry/components/avatar.tsx +59 -7
  27. package/registry/components/badge.tsx +29 -14
  28. package/registry/components/breadcrumb.tsx +7 -13
  29. package/registry/components/button-group.tsx +28 -0
  30. package/registry/components/button.tsx +113 -28
  31. package/registry/components/calendar.tsx +92 -0
  32. package/registry/components/callout.tsx +14 -14
  33. package/registry/components/card.tsx +87 -12
  34. package/registry/components/carousel.tsx +41 -0
  35. package/registry/components/chart.tsx +50 -0
  36. package/registry/components/checkbox.tsx +5 -5
  37. package/registry/components/code-block.tsx +118 -0
  38. package/registry/components/code.tsx +2 -3
  39. package/registry/components/collapsible.tsx +60 -0
  40. package/registry/components/combobox.tsx +102 -0
  41. package/registry/components/command.tsx +5 -5
  42. package/registry/components/context-menu.tsx +81 -0
  43. package/registry/components/data-table.tsx +71 -0
  44. package/registry/components/date-picker.tsx +58 -0
  45. package/registry/components/dialog.tsx +2 -2
  46. package/registry/components/direction.tsx +17 -0
  47. package/registry/components/drawer.tsx +77 -0
  48. package/registry/components/dropdown-menu.tsx +5 -5
  49. package/registry/components/empty.tsx +34 -0
  50. package/registry/components/field.tsx +27 -0
  51. package/registry/components/file-upload.tsx +116 -0
  52. package/registry/components/form.tsx +3 -4
  53. package/registry/components/hover-card.tsx +59 -0
  54. package/registry/components/input-group.tsx +34 -0
  55. package/registry/components/input-otp.tsx +50 -0
  56. package/registry/components/input.tsx +71 -7
  57. package/registry/components/item.tsx +42 -0
  58. package/registry/components/kbd.tsx +3 -4
  59. package/registry/components/label.tsx +34 -4
  60. package/registry/components/menubar.tsx +60 -0
  61. package/registry/components/native-select.tsx +35 -0
  62. package/registry/components/navigation-menu.tsx +3 -3
  63. package/registry/components/pagination.tsx +4 -5
  64. package/registry/components/popover.tsx +1 -1
  65. package/registry/components/progress.tsx +10 -5
  66. package/registry/components/radio-group.tsx +9 -9
  67. package/registry/components/resizable.tsx +77 -0
  68. package/registry/components/scroll-area.tsx +20 -0
  69. package/registry/components/select.tsx +2 -3
  70. package/registry/components/separator.tsx +1 -2
  71. package/registry/components/sheet.tsx +1 -1
  72. package/registry/components/sidebar.tsx +72 -0
  73. package/registry/components/skeleton.tsx +1 -6
  74. package/registry/components/slider.tsx +6 -3
  75. package/registry/components/sonner.tsx +52 -0
  76. package/registry/components/spinner.tsx +19 -6
  77. package/registry/components/stepper.tsx +63 -0
  78. package/registry/components/switch.tsx +7 -6
  79. package/registry/components/table.tsx +2 -3
  80. package/registry/components/tabs.tsx +3 -3
  81. package/registry/components/textarea.tsx +42 -6
  82. package/registry/components/toast.tsx +2 -2
  83. package/registry/components/toggle-group.tsx +72 -0
  84. package/registry/components/toggle.tsx +45 -20
  85. 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-[var(--ui-border)] bg-[var(--ui-surface)] p-6 shadow-lg rounded-xl",
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-[var(--ui-fg-muted)]", className)} {...props} />;
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-[var(--ui-border)] bg-[var(--ui-surface)] p-1 text-[var(--ui-fg)] shadow-md",
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-none transition-colors",
82
- "hover:bg-[var(--ui-surface-2)] focus:bg-[var(--ui-surface-2)]",
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-[var(--ui-border)]" />;
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-[var(--ui-fg-subtle)]", className)} {...props} />;
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-[var(--ui-fg)]">{label}</label> : null}
21
+ {label ? <label className="text-sm font-medium text-foreground">{label}</label> : null}
23
22
  {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}
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
- export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
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
- ({ className, type = "text", ...props }, ref) => (
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 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",
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<HTMLSpanElement, React.HTMLAttributes<HTMLSpanElement>>(
4
+ export const Kbd = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
6
5
  ({ className, ...props }, ref) => (
7
- <span
6
+ <kbd
8
7
  ref={ref}
9
8
  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",
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}