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