@swift-rust/ui 0.3.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 (82) 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/registry.test.d.ts +2 -0
  13. package/dist/registry.test.d.ts.map +1 -0
  14. package/dist/registry.test.js +82 -0
  15. package/dist/registry.test.js.map +1 -0
  16. package/dist/smoke.test.js +5 -3
  17. package/dist/smoke.test.js.map +1 -1
  18. package/package.json +2 -2
  19. package/registry/components/accordion.tsx +125 -16
  20. package/registry/components/alert-dialog.tsx +102 -0
  21. package/registry/components/alert.tsx +114 -14
  22. package/registry/components/aspect-ratio.tsx +18 -0
  23. package/registry/components/avatar.tsx +59 -7
  24. package/registry/components/badge.tsx +29 -14
  25. package/registry/components/breadcrumb.tsx +7 -13
  26. package/registry/components/button-group.tsx +28 -0
  27. package/registry/components/button.tsx +113 -28
  28. package/registry/components/calendar.tsx +92 -0
  29. package/registry/components/callout.tsx +14 -14
  30. package/registry/components/card.tsx +87 -12
  31. package/registry/components/carousel.tsx +41 -0
  32. package/registry/components/chart.tsx +50 -0
  33. package/registry/components/checkbox.tsx +5 -5
  34. package/registry/components/code-block.tsx +118 -0
  35. package/registry/components/code.tsx +2 -3
  36. package/registry/components/collapsible.tsx +60 -0
  37. package/registry/components/combobox.tsx +102 -0
  38. package/registry/components/command.tsx +5 -5
  39. package/registry/components/context-menu.tsx +81 -0
  40. package/registry/components/data-table.tsx +71 -0
  41. package/registry/components/date-picker.tsx +58 -0
  42. package/registry/components/dialog.tsx +2 -2
  43. package/registry/components/direction.tsx +17 -0
  44. package/registry/components/drawer.tsx +77 -0
  45. package/registry/components/dropdown-menu.tsx +5 -5
  46. package/registry/components/empty.tsx +34 -0
  47. package/registry/components/field.tsx +27 -0
  48. package/registry/components/file-upload.tsx +116 -0
  49. package/registry/components/form.tsx +3 -4
  50. package/registry/components/hover-card.tsx +59 -0
  51. package/registry/components/input-group.tsx +34 -0
  52. package/registry/components/input-otp.tsx +50 -0
  53. package/registry/components/input.tsx +71 -7
  54. package/registry/components/item.tsx +42 -0
  55. package/registry/components/kbd.tsx +3 -4
  56. package/registry/components/label.tsx +34 -4
  57. package/registry/components/menubar.tsx +60 -0
  58. package/registry/components/native-select.tsx +35 -0
  59. package/registry/components/navigation-menu.tsx +3 -3
  60. package/registry/components/pagination.tsx +4 -5
  61. package/registry/components/popover.tsx +1 -1
  62. package/registry/components/progress.tsx +10 -5
  63. package/registry/components/radio-group.tsx +9 -9
  64. package/registry/components/resizable.tsx +77 -0
  65. package/registry/components/scroll-area.tsx +20 -0
  66. package/registry/components/select.tsx +2 -3
  67. package/registry/components/separator.tsx +1 -2
  68. package/registry/components/sheet.tsx +1 -1
  69. package/registry/components/sidebar.tsx +72 -0
  70. package/registry/components/skeleton.tsx +1 -6
  71. package/registry/components/slider.tsx +6 -3
  72. package/registry/components/sonner.tsx +52 -0
  73. package/registry/components/spinner.tsx +19 -6
  74. package/registry/components/stepper.tsx +63 -0
  75. package/registry/components/switch.tsx +7 -6
  76. package/registry/components/table.tsx +2 -3
  77. package/registry/components/tabs.tsx +3 -3
  78. package/registry/components/textarea.tsx +42 -6
  79. package/registry/components/toast.tsx +2 -2
  80. package/registry/components/toggle-group.tsx +72 -0
  81. package/registry/components/toggle.tsx +45 -20
  82. package/registry/components/tooltip.tsx +4 -2
@@ -1,12 +1,57 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- const Avatar = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
- ({ className, ...props }, ref) => (
4
+ /**
5
+ * swift-rust ui · Avatar
6
+ *
7
+ * size — xs, sm, default, md, lg
8
+ * design — flat, soft, 3d, glass, neo, brutal, gradient
9
+ *
10
+ * The gradient design renders a gradient ring; brutal squares the corners.
11
+ */
12
+
13
+ export type AvatarSize = "default" | "xs" | "sm" | "md" | "lg";
14
+ export type AvatarDesign = "flat" | "soft" | "3d" | "glass" | "neo" | "brutal" | "gradient";
15
+
16
+ const SIZES: Record<AvatarSize, string> = {
17
+ xs: "size-6 text-[10px]",
18
+ sm: "size-8 text-xs",
19
+ default: "size-10 text-sm",
20
+ md: "size-10 text-sm",
21
+ lg: "size-12 text-base",
22
+ };
23
+
24
+ const DESIGNS: Record<AvatarDesign, string> = {
25
+ flat: "",
26
+ soft: "ring-4 ring-secondary",
27
+ // Depth via a darker bottom lip + a sheen over the fallback face.
28
+ "3d": "border-b-[3px] border-b-black/30 bg-linear-to-b from-white/30 to-black/10 dark:border-b-black/50",
29
+ // Liquid glass ring with vibrancy.
30
+ glass: "ring-2 ring-white/50 backdrop-blur-xl backdrop-saturate-200 shadow-[0_2px_10px_rgba(31,38,135,0.2)] dark:ring-white/25",
31
+ neo:
32
+ "shadow-[4px_4px_8px_rgba(0,0,0,0.15),-4px_-4px_8px_rgba(255,255,255,0.8)] " +
33
+ "dark:shadow-[4px_4px_8px_rgba(0,0,0,0.6),-4px_-4px_8px_rgba(255,255,255,0.06)]",
34
+ brutal:
35
+ "rounded-none border-2 border-foreground shadow-[3px_3px_0_0_var(--color-foreground)] " +
36
+ "[&_[data-avatar-img]]:rounded-none [&_[data-avatar-fallback]]:rounded-none",
37
+ gradient: "p-0.5 bg-linear-to-br from-violet-500 via-fuchsia-500 to-orange-400",
38
+ };
39
+
40
+ export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
41
+ size?: AvatarSize;
42
+ design?: AvatarDesign;
43
+ }
44
+
45
+ const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
46
+ ({ className, size = "default", design = "flat", ...props }, ref) => (
7
47
  <div
8
48
  ref={ref}
9
- className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
49
+ className={cn(
50
+ "relative flex shrink-0 overflow-hidden rounded-full",
51
+ SIZES[size],
52
+ DESIGNS[design],
53
+ className,
54
+ )}
10
55
  {...props}
11
56
  />
12
57
  ),
@@ -14,8 +59,14 @@ const Avatar = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElem
14
59
  Avatar.displayName = "Avatar";
15
60
 
16
61
  const AvatarImage = React.forwardRef<HTMLImageElement, React.ImgHTMLAttributes<HTMLImageElement>>(
17
- ({ className, ...props }, ref) => (
18
- <img ref={ref} className={cn("aspect-square h-full w-full object-cover", className)} {...props} />
62
+ ({ className, alt = "", ...props }, ref) => (
63
+ <img
64
+ ref={ref}
65
+ data-avatar-img
66
+ alt={alt}
67
+ className={cn("aspect-square size-full rounded-full object-cover", className)}
68
+ {...props}
69
+ />
19
70
  ),
20
71
  );
21
72
  AvatarImage.displayName = "AvatarImage";
@@ -24,8 +75,9 @@ const AvatarFallback = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTM
24
75
  ({ className, ...props }, ref) => (
25
76
  <div
26
77
  ref={ref}
78
+ data-avatar-fallback
27
79
  className={cn(
28
- "flex h-full w-full items-center justify-center rounded-full bg-[var(--ui-surface-2)] text-sm font-medium",
80
+ "flex size-full items-center justify-center rounded-full bg-secondary font-medium text-secondary-foreground",
29
81
  className,
30
82
  )}
31
83
  {...props}
@@ -1,27 +1,42 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- type Variant = "default" | "secondary" | "destructive" | "outline" | "success" | "warning";
6
- const VARIANTS: Record<Variant, string> = {
7
- default: "border-transparent bg-[var(--ui-fg)] text-[var(--ui-bg)]",
8
- secondary: "border-transparent bg-[var(--ui-surface-2)] text-[var(--ui-fg)]",
9
- destructive: "border-transparent bg-[var(--ui-danger)] text-white",
10
- outline: "border-[var(--ui-border-strong)] text-[var(--ui-fg)]",
11
- success: "border-transparent bg-[#dcfce7] text-[#166534]",
12
- warning: "border-transparent bg-[#fef3c7] text-[#92400e]",
4
+ export type BadgeVariant =
5
+ | "default"
6
+ | "secondary"
7
+ | "destructive"
8
+ | "outline"
9
+ | "success"
10
+ | "warning";
11
+ export type BadgeSize = "default" | "sm" | "lg";
12
+
13
+ const VARIANTS: Record<BadgeVariant, string> = {
14
+ default: "border-transparent bg-primary text-primary-foreground",
15
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
16
+ destructive: "border-transparent bg-destructive text-destructive-foreground",
17
+ outline: "border-border text-foreground",
18
+ success: "border-transparent bg-emerald-500/15 text-emerald-700 dark:text-emerald-400",
19
+ warning: "border-transparent bg-amber-500/15 text-amber-700 dark:text-amber-400",
20
+ };
21
+
22
+ const SIZES: Record<BadgeSize, string> = {
23
+ default: "px-2.5 py-0.5 text-xs",
24
+ sm: "px-2 py-0 text-[0.65rem]",
25
+ lg: "px-3 py-1 text-sm",
13
26
  };
14
27
 
15
- export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
16
- variant?: Variant;
28
+ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
29
+ variant?: BadgeVariant;
30
+ size?: BadgeSize;
17
31
  }
18
32
 
19
- export function Badge({ className, variant = "default", ...props }: BadgeProps) {
33
+ export function Badge({ className, variant = "default", size = "default", ...props }: BadgeProps) {
20
34
  return (
21
- <div
35
+ <span
22
36
  className={cn(
23
- "inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium",
37
+ "inline-flex items-center gap-1 rounded-full border font-medium [&_svg]:size-3",
24
38
  VARIANTS[variant],
39
+ SIZES[size],
25
40
  className,
26
41
  )}
27
42
  {...props}
@@ -1,4 +1,3 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
@@ -13,11 +12,11 @@ export function Breadcrumb({
13
12
  children: React.ReactNode;
14
13
  className?: string;
15
14
  }) {
16
- const sep = separator ?? <span className="text-[var(--ui-fg-subtle)]">/</span>;
15
+ const sep = separator ?? <span className="text-muted-foreground">/</span>;
17
16
  return (
18
17
  <BreadcrumbContext.Provider value={{ separator: sep }}>
19
18
  <nav aria-label="Breadcrumb" className={className}>
20
- <ol className="flex flex-wrap items-center gap-1.5 text-sm text-[var(--ui-fg-muted)]">
19
+ <ol className="flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground">
21
20
  {children}
22
21
  </ol>
23
22
  </nav>
@@ -26,30 +25,25 @@ export function Breadcrumb({
26
25
  }
27
26
 
28
27
  export function BreadcrumbItem({ children, className }: { children: React.ReactNode; className?: string }) {
29
- const ctx = React.useContext(BreadcrumbContext)!;
30
- return (
31
- <li className={cn("inline-flex items-center gap-1.5", className)}>
32
- {children}
33
- </li>
34
- );
28
+ return <li className={cn("inline-flex items-center gap-1.5", className)}>{children}</li>;
35
29
  }
36
30
 
37
31
  export function BreadcrumbLink({ href, children }: { href: string; children: React.ReactNode }) {
38
32
  return (
39
- <a href={href} className="hover:text-[var(--ui-fg)]">
33
+ <a href={href} className="transition-colors hover:text-foreground">
40
34
  {children}
41
35
  </a>
42
36
  );
43
37
  }
44
38
 
45
39
  export function BreadcrumbSeparator() {
46
- const ctx = React.useContext(BreadcrumbContext)!;
47
- return <li aria-hidden>{ctx.separator}</li>;
40
+ const ctx = React.useContext(BreadcrumbContext);
41
+ return <li aria-hidden>{ctx?.separator ?? "/"}</li>;
48
42
  }
49
43
 
50
44
  export function BreadcrumbPage({ children }: { children: React.ReactNode }) {
51
45
  return (
52
- <li aria-current="page" className="font-medium text-[var(--ui-fg)]">
46
+ <li aria-current="page" className="font-medium text-foreground">
53
47
  {children}
54
48
  </li>
55
49
  );
@@ -0,0 +1,28 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ // Joins a row of <Button>s into a single segmented control: shared borders,
5
+ // rounded only on the outer ends.
6
+ export interface ButtonGroupProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ orientation?: "horizontal" | "vertical";
8
+ }
9
+
10
+ export const ButtonGroup = React.forwardRef<HTMLDivElement, ButtonGroupProps>(
11
+ ({ className, orientation = "horizontal", ...props }, ref) => (
12
+ <div
13
+ ref={ref}
14
+ role="group"
15
+ data-orientation={orientation}
16
+ className={cn(
17
+ "inline-flex isolate",
18
+ orientation === "horizontal"
19
+ ? "flex-row [&>*]:rounded-none [&>*:not(:first-child)]:-ml-px [&>*:first-child]:rounded-l-lg [&>*:last-child]:rounded-r-lg"
20
+ : "flex-col [&>*]:rounded-none [&>*:not(:first-child)]:-mt-px [&>*:first-child]:rounded-t-lg [&>*:last-child]:rounded-b-lg",
21
+ "[&>*:hover]:z-10 [&>*:focus-visible]:z-10",
22
+ className,
23
+ )}
24
+ {...props}
25
+ />
26
+ ),
27
+ );
28
+ ButtonGroup.displayName = "ButtonGroup";
@@ -1,48 +1,133 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- type ButtonVariant = "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
6
- type ButtonSize = "default" | "sm" | "lg" | "icon";
4
+ /**
5
+ * swift-rust ui · Button
6
+ *
7
+ * Three independent dimensions (Tailwind v4):
8
+ * variant — what the button means (default, outline, secondary, ghost, destructive, link)
9
+ * size — how big it is (default, xs, sm, md, lg, icon, icon-xs, icon-sm, icon-md, icon-lg)
10
+ * design — how it looks (flat, soft, 3d, glass, neo, brutal, gradient)
11
+ *
12
+ * `design` is the "style" dimension — the prop is named `design` because React
13
+ * reserves `style` for inline styles.
14
+ */
15
+
16
+ export type ButtonVariant = "default" | "outline" | "secondary" | "ghost" | "destructive" | "link";
17
+ export type ButtonSize =
18
+ | "default"
19
+ | "xs"
20
+ | "sm"
21
+ | "md"
22
+ | "lg"
23
+ | "icon"
24
+ | "icon-xs"
25
+ | "icon-sm"
26
+ | "icon-md"
27
+ | "icon-lg";
28
+ export type ButtonDesign = "flat" | "soft" | "3d" | "glass" | "neo" | "brutal" | "gradient";
7
29
 
8
30
  const VARIANTS: Record<ButtonVariant, string> = {
9
- default: "bg-[var(--ui-fg)] text-[var(--ui-bg)] hover:opacity-90",
10
- destructive: "bg-[var(--ui-danger)] text-white hover:opacity-90",
11
- outline: "border border-[var(--ui-border-strong)] bg-transparent hover:bg-[var(--ui-surface-2)]",
12
- secondary: "bg-[var(--ui-surface-2)] text-[var(--ui-fg)] hover:opacity-90",
13
- ghost: "hover:bg-[var(--ui-surface-2)]",
14
- link: "text-[var(--ui-accent)] underline-offset-4 hover:underline",
31
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
32
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
33
+ outline: "border border-input bg-background text-foreground hover:bg-secondary",
34
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
35
+ ghost: "text-foreground hover:bg-secondary",
36
+ link: "text-primary underline-offset-4 hover:underline",
15
37
  };
16
38
 
17
39
  const SIZES: Record<ButtonSize, string> = {
18
40
  default: "h-9 px-4 text-sm",
19
- sm: "h-8 px-3 text-xs",
41
+ xs: "h-7 gap-1 rounded-md px-2.5 text-xs",
42
+ sm: "h-8 gap-1.5 px-3 text-xs",
43
+ md: "h-9 px-4 text-sm",
20
44
  lg: "h-10 px-6 text-sm",
21
- icon: "h-9 w-9",
45
+ icon: "size-9",
46
+ "icon-xs": "size-7 rounded-md",
47
+ "icon-sm": "size-8",
48
+ "icon-md": "size-9",
49
+ "icon-lg": "size-10",
50
+ };
51
+
52
+ const DESIGNS: Record<ButtonDesign, string> = {
53
+ flat: "",
54
+ soft: "rounded-xl border-transparent shadow-none",
55
+ // Real depth, not a drop shadow: a glossy top→bottom sheen layered over the
56
+ // variant color gives the face a curve, and a darker bottom "lip" (a thick
57
+ // bottom border, not a shadow) makes it sit proud of the page. Pressing
58
+ // sinks it — the lip shrinks and the whole face shifts down.
59
+ "3d":
60
+ "border-b-4 border-b-black/30 bg-linear-to-b from-white/25 to-black/10 " +
61
+ "hover:brightness-110 active:translate-y-[3px] active:border-b-[1px] active:brightness-100 " +
62
+ "dark:border-b-black/50",
63
+ // Liquid glass: heavy blur + saturation for vibrancy, a translucent refractive
64
+ // sheen (the diagonal white gradient), a bright rim, and a specular top
65
+ // highlight with a soft ambient lift.
66
+ glass:
67
+ "border border-white/40 bg-white/15 text-foreground backdrop-blur-xl backdrop-saturate-200 " +
68
+ "bg-linear-to-br from-white/35 via-white/10 to-white/5 hover:bg-white/25 " +
69
+ "shadow-[inset_0_1px_1px_rgba(255,255,255,0.6),0_4px_16px_rgba(31,38,135,0.18)] " +
70
+ "dark:border-white/20 dark:bg-white/10 dark:from-white/20 dark:via-white/5 dark:to-transparent " +
71
+ "dark:shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),0_4px_16px_rgba(0,0,0,0.4)]",
72
+ neo:
73
+ "border-transparent bg-background text-foreground " +
74
+ "shadow-[5px_5px_10px_rgba(0,0,0,0.15),-5px_-5px_10px_rgba(255,255,255,0.8)] " +
75
+ "hover:shadow-[3px_3px_6px_rgba(0,0,0,0.15),-3px_-3px_6px_rgba(255,255,255,0.8)] " +
76
+ "active:shadow-[inset_3px_3px_6px_rgba(0,0,0,0.15),inset_-3px_-3px_6px_rgba(255,255,255,0.7)] " +
77
+ "dark:shadow-[5px_5px_10px_rgba(0,0,0,0.6),-5px_-5px_10px_rgba(255,255,255,0.06)] " +
78
+ "dark:hover:shadow-[3px_3px_6px_rgba(0,0,0,0.6),-3px_-3px_6px_rgba(255,255,255,0.06)] " +
79
+ "dark:active:shadow-[inset_3px_3px_6px_rgba(0,0,0,0.6),inset_-3px_-3px_6px_rgba(255,255,255,0.05)]",
80
+ brutal:
81
+ "rounded-none border-2 border-foreground font-semibold " +
82
+ "shadow-[4px_4px_0_0_var(--color-foreground)] " +
83
+ "active:translate-x-[2px] active:translate-y-[2px] active:shadow-[1px_1px_0_0_var(--color-foreground)]",
84
+ gradient:
85
+ "border-transparent bg-linear-to-br from-violet-500 via-fuchsia-500 to-orange-400 " +
86
+ "text-white shadow-md hover:brightness-110 active:brightness-95",
22
87
  };
23
88
 
24
- export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
89
+ // Designs that paint their own surface keep the variant's text/intent where
90
+ // possible; link buttons have no surface, so designs are skipped for them.
91
+ export interface ButtonStyleProps {
25
92
  variant?: ButtonVariant;
26
93
  size?: ButtonSize;
94
+ design?: ButtonDesign;
95
+ }
96
+
97
+ export function buttonVariants({
98
+ variant = "default",
99
+ size = "default",
100
+ design = "flat",
101
+ className,
102
+ }: ButtonStyleProps & { className?: string } = {}): string {
103
+ return cn(
104
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium transition-all",
105
+ "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
106
+ "disabled:pointer-events-none disabled:opacity-50",
107
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
108
+ VARIANTS[variant],
109
+ SIZES[size],
110
+ variant === "link" ? "" : DESIGNS[design],
111
+ className,
112
+ );
113
+ }
114
+
115
+ export interface ButtonProps
116
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
117
+ ButtonStyleProps {
27
118
  asChild?: boolean;
28
119
  }
29
120
 
30
121
  export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
31
- ({ className, variant = "default", size = "default", ...props }, ref) => (
32
- <button
33
- ref={ref}
34
- className={cn(
35
- "inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors",
36
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-accent)] focus-visible:ring-offset-2",
37
- "disabled:pointer-events-none disabled:opacity-50",
38
- VARIANTS[variant],
39
- SIZES[size],
40
- className,
41
- )}
42
- {...props}
43
- />
44
- ),
122
+ ({ className, variant = "default", size = "default", design = "flat", asChild, ...props }, ref) => {
123
+ const classes = buttonVariants({ variant, size, design, className });
124
+ if (asChild && React.isValidElement(props.children)) {
125
+ const child = props.children as React.ReactElement<{ className?: string }>;
126
+ return React.cloneElement(child, {
127
+ className: cn(classes, child.props.className),
128
+ });
129
+ }
130
+ return <button ref={ref} className={classes} {...props} />;
131
+ },
45
132
  );
46
133
  Button.displayName = "Button";
47
-
48
- export { VARIANTS as buttonVariants };
@@ -0,0 +1,92 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
6
+ const MONTHS = [
7
+ "January", "February", "March", "April", "May", "June",
8
+ "July", "August", "September", "October", "November", "December",
9
+ ];
10
+
11
+ function sameDay(a: Date, b: Date) {
12
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
13
+ }
14
+
15
+ export function Calendar({
16
+ value: controlled,
17
+ defaultValue,
18
+ onChange,
19
+ className,
20
+ }: {
21
+ value?: Date | null;
22
+ defaultValue?: Date;
23
+ onChange?: (d: Date) => void;
24
+ className?: string;
25
+ }) {
26
+ const [selected, setSelected] = React.useState<Date | null>(defaultValue ?? null);
27
+ const value = controlled !== undefined ? controlled : selected;
28
+ const [view, setView] = React.useState<Date>(() => {
29
+ const base = controlled ?? defaultValue ?? new Date();
30
+ return new Date(base.getFullYear(), base.getMonth(), 1);
31
+ });
32
+
33
+ const year = view.getFullYear();
34
+ const month = view.getMonth();
35
+ const firstDay = new Date(year, month, 1).getDay();
36
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
37
+ const today = new Date();
38
+
39
+ const cells: (number | null)[] = [
40
+ ...Array.from({ length: firstDay }, () => null),
41
+ ...Array.from({ length: daysInMonth }, (_, i) => i + 1),
42
+ ];
43
+
44
+ const pick = (day: number) => {
45
+ const d = new Date(year, month, day);
46
+ if (controlled === undefined) setSelected(d);
47
+ onChange?.(d);
48
+ };
49
+
50
+ const shift = (n: number) => setView(new Date(year, month + n, 1));
51
+
52
+ return (
53
+ <div className={cn("w-fit rounded-xl border border-border bg-card p-3 text-card-foreground", className)}>
54
+ <div className="mb-2 flex items-center justify-between px-1">
55
+ <button type="button" onClick={() => shift(-1)} aria-label="Previous month" className="inline-flex size-7 items-center justify-center rounded-md hover:bg-muted">
56
+ <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden><path d="m15 18-6-6 6-6" strokeLinecap="round" strokeLinejoin="round" /></svg>
57
+ </button>
58
+ <span className="text-sm font-medium">{MONTHS[month]} {year}</span>
59
+ <button type="button" onClick={() => shift(1)} aria-label="Next month" className="inline-flex size-7 items-center justify-center rounded-md hover:bg-muted">
60
+ <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden><path d="m9 18 6-6-6-6" strokeLinecap="round" strokeLinejoin="round" /></svg>
61
+ </button>
62
+ </div>
63
+ <div className="grid grid-cols-7 gap-1">
64
+ {WEEKDAYS.map((d) => (
65
+ <div key={d} className="flex h-8 items-center justify-center text-xs font-medium text-muted-foreground">{d}</div>
66
+ ))}
67
+ {cells.map((day, i) =>
68
+ day === null ? (
69
+ // eslint-disable-next-line react/no-array-index-key
70
+ <div key={`e${i}`} />
71
+ ) : (
72
+ <button
73
+ key={day}
74
+ type="button"
75
+ onClick={() => pick(day)}
76
+ className={cn(
77
+ "flex size-8 items-center justify-center rounded-md text-sm transition-colors hover:bg-muted",
78
+ value && sameDay(value, new Date(year, month, day))
79
+ ? "bg-primary text-primary-foreground hover:bg-primary/90"
80
+ : sameDay(today, new Date(year, month, day))
81
+ ? "ring-1 ring-inset ring-border"
82
+ : "",
83
+ )}
84
+ >
85
+ {day}
86
+ </button>
87
+ ),
88
+ )}
89
+ </div>
90
+ </div>
91
+ );
92
+ }
@@ -1,20 +1,20 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- type Tone = "default" | "success" | "warning" | "destructive";
6
- const TONE: Record<Tone, string> = {
7
- default: "border-[var(--ui-border)] bg-[var(--ui-surface)]",
8
- success: "border-[#86efac] bg-[#f0fdf4]",
9
- warning: "border-[#fde68a] bg-[#fffbeb]",
10
- destructive: "border-[#fecaca] bg-[#fef2f2]",
4
+ export type CalloutTone = "default" | "info" | "success" | "warning" | "destructive";
5
+
6
+ const TONE: Record<CalloutTone, string> = {
7
+ default: "border-border bg-card text-card-foreground",
8
+ info: "border-sky-600/30 bg-sky-500/10 text-sky-700 dark:text-sky-400",
9
+ success: "border-emerald-600/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400",
10
+ warning: "border-amber-600/30 bg-amber-500/10 text-amber-700 dark:text-amber-400",
11
+ destructive: "border-destructive/30 bg-destructive/10 text-destructive",
11
12
  };
12
13
 
13
- export function Callout({ className, tone = "default", ...props }: React.HTMLAttributes<HTMLDivElement> & { tone?: Tone }) {
14
- return (
15
- <div
16
- className={cn("my-4 rounded-lg border p-4 text-sm", TONE[tone], className)}
17
- {...props}
18
- />
19
- );
14
+ export function Callout({
15
+ className,
16
+ tone = "default",
17
+ ...props
18
+ }: React.HTMLAttributes<HTMLDivElement> & { tone?: CalloutTone }) {
19
+ return <div className={cn("my-4 rounded-lg border p-4 text-sm", TONE[tone], className)} {...props} />;
20
20
  }
@@ -1,12 +1,73 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
- ({ className, ...props }, ref) => (
4
+ /**
5
+ * swift-rust ui · Card
6
+ *
7
+ * variant — default, outline, secondary, ghost
8
+ * size — sm, default, lg (padding scale; subcomponents read it via --card-p)
9
+ * design — flat, soft, 3d, glass, neo, brutal, gradient
10
+ */
11
+
12
+ export type CardVariant = "default" | "outline" | "secondary" | "ghost";
13
+ export type CardSize = "default" | "sm" | "lg";
14
+ export type CardDesign = "flat" | "soft" | "3d" | "glass" | "neo" | "brutal" | "gradient";
15
+
16
+ const VARIANTS: Record<CardVariant, string> = {
17
+ default: "border border-border bg-card text-card-foreground",
18
+ outline: "border-2 border-border bg-transparent text-foreground",
19
+ secondary: "border border-transparent bg-secondary text-secondary-foreground",
20
+ ghost: "border border-transparent bg-transparent text-foreground",
21
+ };
22
+
23
+ const SIZES: Record<CardSize, string> = {
24
+ sm: "[--card-p:1rem]",
25
+ default: "[--card-p:1.5rem]",
26
+ lg: "[--card-p:2rem]",
27
+ };
28
+
29
+ const DESIGNS: Record<CardDesign, string> = {
30
+ flat: "shadow-sm",
31
+ soft: "rounded-2xl border-transparent bg-muted shadow-none",
32
+ // Depth via a darker bottom lip (border, not shadow) + a top sheen, so the
33
+ // card sits proud of the page like a physical tile.
34
+ "3d": "border-b-[6px] border-b-black/20 bg-linear-to-b from-white/30 to-transparent dark:border-b-black/50",
35
+ // Liquid glass: heavy blur + saturation, a translucent refractive sheen, a
36
+ // bright rim, and a specular top highlight over a soft ambient lift.
37
+ glass:
38
+ "border-white/40 bg-white/15 backdrop-blur-xl backdrop-saturate-200 " +
39
+ "bg-linear-to-br from-white/35 via-white/10 to-white/5 " +
40
+ "shadow-[inset_0_1px_1px_rgba(255,255,255,0.6),0_8px_32px_rgba(31,38,135,0.18)] " +
41
+ "dark:border-white/20 dark:bg-white/10 dark:from-white/15 dark:via-white/5 dark:to-transparent " +
42
+ "dark:shadow-[inset_0_1px_1px_rgba(255,255,255,0.25),0_8px_32px_rgba(0,0,0,0.45)]",
43
+ neo:
44
+ "border-transparent bg-background " +
45
+ "shadow-[8px_8px_16px_rgba(0,0,0,0.15),-8px_-8px_16px_rgba(255,255,255,0.8)] " +
46
+ "dark:shadow-[8px_8px_16px_rgba(0,0,0,0.6),-8px_-8px_16px_rgba(255,255,255,0.05)]",
47
+ brutal: "rounded-none border-2 border-foreground shadow-[6px_6px_0_0_var(--color-foreground)]",
48
+ gradient:
49
+ "border-2 border-transparent " +
50
+ "[background:linear-gradient(var(--color-card),var(--color-card))_padding-box," +
51
+ "linear-gradient(135deg,#8b5cf6,#d946ef,#fb923c)_border-box] shadow-md",
52
+ };
53
+
54
+ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
55
+ variant?: CardVariant;
56
+ size?: CardSize;
57
+ design?: CardDesign;
58
+ }
59
+
60
+ const Card = React.forwardRef<HTMLDivElement, CardProps>(
61
+ ({ className, variant = "default", size = "default", design = "flat", ...props }, ref) => (
7
62
  <div
8
63
  ref={ref}
9
- className={cn("rounded-xl border border-[var(--ui-border)] bg-[var(--ui-surface)] text-[var(--ui-fg)] shadow-sm", className)}
64
+ className={cn(
65
+ "flex flex-col rounded-xl",
66
+ VARIANTS[variant],
67
+ SIZES[size],
68
+ DESIGNS[design],
69
+ className,
70
+ )}
10
71
  {...props}
11
72
  />
12
73
  ),
@@ -15,35 +76,49 @@ Card.displayName = "Card";
15
76
 
16
77
  const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
78
  ({ className, ...props }, ref) => (
18
- <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
79
+ <div ref={ref} className={cn("flex flex-col gap-1.5 p-(--card-p)", className)} {...props} />
19
80
  ),
20
81
  );
21
82
  CardHeader.displayName = "CardHeader";
22
83
 
23
84
  const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
24
85
  ({ className, ...props }, ref) => (
25
- <h3 ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
86
+ <h3
87
+ ref={ref}
88
+ className={cn("text-lg font-semibold leading-none tracking-tight", className)}
89
+ {...props}
90
+ />
26
91
  ),
27
92
  );
28
93
  CardTitle.displayName = "CardTitle";
29
94
 
30
- const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
95
+ const CardDescription = React.forwardRef<
96
+ HTMLParagraphElement,
97
+ React.HTMLAttributes<HTMLParagraphElement>
98
+ >(({ className, ...props }, ref) => (
99
+ <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
100
+ ));
101
+ CardDescription.displayName = "CardDescription";
102
+
103
+ const CardAction = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
31
104
  ({ className, ...props }, ref) => (
32
- <p ref={ref} className={cn("text-sm text-[var(--ui-fg-muted)]", className)} {...props} />
105
+ <div ref={ref} className={cn("ml-auto self-start", className)} {...props} />
33
106
  ),
34
107
  );
35
- CardDescription.displayName = "CardDescription";
108
+ CardAction.displayName = "CardAction";
36
109
 
37
110
  const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
38
- ({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
111
+ ({ className, ...props }, ref) => (
112
+ <div ref={ref} className={cn("p-(--card-p) pt-0", className)} {...props} />
113
+ ),
39
114
  );
40
115
  CardContent.displayName = "CardContent";
41
116
 
42
117
  const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
43
118
  ({ className, ...props }, ref) => (
44
- <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
119
+ <div ref={ref} className={cn("flex items-center p-(--card-p) pt-0", className)} {...props} />
45
120
  ),
46
121
  );
47
122
  CardFooter.displayName = "CardFooter";
48
123
 
49
- export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
124
+ export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };