create-swift-rust 0.8.2 → 0.9.1

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.
@@ -0,0 +1,215 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ /**
6
+ * swift-rust ui · Accordion
7
+ *
8
+ * variant — default (divided), outline (separated items), secondary, ghost
9
+ * size — sm, default, lg
10
+ * design — flat, soft, 3d, glass, neo, brutal, gradient
11
+ *
12
+ * variant/size/design are set once on <Accordion> and flow to items via context.
13
+ */
14
+
15
+ export type AccordionVariant = "default" | "outline" | "secondary" | "ghost";
16
+ export type AccordionSize = "default" | "sm" | "lg";
17
+ export type AccordionDesign = "flat" | "soft" | "3d" | "glass" | "neo" | "brutal" | "gradient";
18
+
19
+ // Item chrome. The default variant is a classic divided list; the others render
20
+ // separated, surfaced items so designs (glass, brutal, …) have a card to paint.
21
+ const ITEM_VARIANTS: Record<AccordionVariant, string> = {
22
+ default: "border-b border-border last:border-b-0",
23
+ outline: "mb-2 rounded-lg border border-border px-4 last:mb-0",
24
+ secondary: "mb-2 rounded-lg bg-secondary px-4 text-secondary-foreground last:mb-0",
25
+ ghost: "rounded-lg px-2 hover:bg-secondary/50",
26
+ };
27
+
28
+ const TRIGGER_SIZES: Record<AccordionSize, string> = {
29
+ sm: "py-3 text-xs",
30
+ default: "py-4 text-sm",
31
+ lg: "py-5 text-base",
32
+ };
33
+
34
+ const CONTENT_SIZES: Record<AccordionSize, string> = {
35
+ sm: "pb-3 text-xs",
36
+ default: "pb-4 text-sm",
37
+ lg: "pb-5 text-base",
38
+ };
39
+
40
+ // Designs only decorate non-default variants' item surfaces (the divided list
41
+ // has no surface to paint).
42
+ const ITEM_DESIGNS: Record<AccordionDesign, string> = {
43
+ flat: "",
44
+ soft: "rounded-xl border-transparent bg-muted",
45
+ // Depth via a darker bottom lip + a top sheen — no drop shadow.
46
+ "3d": "border-transparent border-b-4 border-b-black/15 bg-linear-to-b from-white/20 to-transparent dark:border-b-black/40",
47
+ // Liquid glass panels.
48
+ glass:
49
+ "border-white/40 bg-white/15 backdrop-blur-xl backdrop-saturate-200 " +
50
+ "bg-linear-to-br from-white/30 via-white/10 to-white/5 " +
51
+ "dark:border-white/20 dark:bg-white/10 dark:from-white/15 dark:via-white/5 dark:to-transparent",
52
+ neo:
53
+ "border-transparent bg-background " +
54
+ "shadow-[5px_5px_10px_rgba(0,0,0,0.12),-5px_-5px_10px_rgba(255,255,255,0.8)] " +
55
+ "dark:shadow-[5px_5px_10px_rgba(0,0,0,0.6),-5px_-5px_10px_rgba(255,255,255,0.05)]",
56
+ brutal:
57
+ "rounded-none border-2 border-foreground shadow-[4px_4px_0_0_var(--color-foreground)]",
58
+ gradient:
59
+ "border-2 border-transparent " +
60
+ "[background:linear-gradient(var(--color-background),var(--color-background))_padding-box," +
61
+ "linear-gradient(135deg,#8b5cf6,#d946ef,#fb923c)_border-box]",
62
+ };
63
+
64
+ interface AccordionContextValue {
65
+ openItems: Set<string>;
66
+ toggle: (value: string) => void;
67
+ type: "single" | "multiple";
68
+ variant: AccordionVariant;
69
+ size: AccordionSize;
70
+ design: AccordionDesign;
71
+ }
72
+
73
+ export const AccordionContext = React.createContext<AccordionContextValue | null>(null);
74
+
75
+ function useAccordion(): AccordionContextValue {
76
+ const ctx = React.useContext(AccordionContext);
77
+ if (!ctx) throw new Error("Accordion components must be used inside <Accordion>");
78
+ return ctx;
79
+ }
80
+
81
+ export interface AccordionProps {
82
+ type?: "single" | "multiple";
83
+ defaultValue?: string | string[];
84
+ variant?: AccordionVariant;
85
+ size?: AccordionSize;
86
+ design?: AccordionDesign;
87
+ className?: string;
88
+ children: React.ReactNode;
89
+ }
90
+
91
+ export function Accordion({
92
+ type = "single",
93
+ defaultValue,
94
+ variant = "default",
95
+ size = "default",
96
+ design = "flat",
97
+ className,
98
+ children,
99
+ }: AccordionProps) {
100
+ const [openItems, setOpenItems] = React.useState<Set<string>>(
101
+ () => new Set(Array.isArray(defaultValue) ? defaultValue : defaultValue ? [defaultValue] : []),
102
+ );
103
+ const toggle = (value: string) => {
104
+ setOpenItems((prev) => {
105
+ const next = new Set(prev);
106
+ if (next.has(value)) next.delete(value);
107
+ else if (type === "single") {
108
+ next.clear();
109
+ next.add(value);
110
+ } else next.add(value);
111
+ return next;
112
+ });
113
+ };
114
+ return (
115
+ <AccordionContext.Provider value={{ openItems, toggle, type, variant, size, design }}>
116
+ <div className={className}>{children}</div>
117
+ </AccordionContext.Provider>
118
+ );
119
+ }
120
+
121
+ export function AccordionItem({
122
+ value,
123
+ disabled,
124
+ className,
125
+ children,
126
+ }: {
127
+ value: string;
128
+ disabled?: boolean;
129
+ className?: string;
130
+ children: React.ReactNode;
131
+ }) {
132
+ const { openItems, variant, design } = useAccordion();
133
+ return (
134
+ <div
135
+ data-value={value}
136
+ data-disabled={disabled ? "" : undefined}
137
+ data-state={openItems.has(value) ? "open" : "closed"}
138
+ className={cn(
139
+ ITEM_VARIANTS[variant],
140
+ variant !== "default" && ITEM_DESIGNS[design],
141
+ variant !== "default" && design !== "flat" && "mb-3 last:mb-0",
142
+ className,
143
+ )}
144
+ >
145
+ {children}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ export function AccordionTrigger({
151
+ value,
152
+ disabled,
153
+ className,
154
+ children,
155
+ }: {
156
+ value: string;
157
+ disabled?: boolean;
158
+ className?: string;
159
+ children: React.ReactNode;
160
+ }) {
161
+ const ctx = useAccordion();
162
+ const open = ctx.openItems.has(value);
163
+ return (
164
+ <button
165
+ type="button"
166
+ disabled={disabled}
167
+ aria-expanded={open}
168
+ data-state={open ? "open" : "closed"}
169
+ onClick={() => ctx.toggle(value)}
170
+ className={cn(
171
+ "flex w-full items-center justify-between gap-4 text-left font-medium transition-all",
172
+ "rounded-md hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
173
+ "disabled:pointer-events-none disabled:opacity-50",
174
+ TRIGGER_SIZES[ctx.size],
175
+ className,
176
+ )}
177
+ >
178
+ {children}
179
+ <svg
180
+ viewBox="0 0 24 24"
181
+ className={cn(
182
+ "size-4 shrink-0 text-muted-foreground transition-transform duration-200",
183
+ open && "rotate-180",
184
+ )}
185
+ fill="none"
186
+ stroke="currentColor"
187
+ strokeWidth="2"
188
+ aria-hidden="true"
189
+ >
190
+ <path d="M6 9l6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
191
+ </svg>
192
+ </button>
193
+ );
194
+ }
195
+
196
+ export function AccordionContent({
197
+ value,
198
+ className,
199
+ children,
200
+ }: {
201
+ value: string;
202
+ className?: string;
203
+ children: React.ReactNode;
204
+ }) {
205
+ const ctx = useAccordion();
206
+ if (!ctx.openItems.has(value)) return null;
207
+ return (
208
+ <div
209
+ data-state="open"
210
+ className={cn("text-muted-foreground", CONTENT_SIZES[ctx.size], className)}
211
+ >
212
+ {children}
213
+ </div>
214
+ );
215
+ }
@@ -0,0 +1,134 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ /**
5
+ * swift-rust ui · Alert
6
+ *
7
+ * variant — default, destructive, success, warning, info, outline, secondary
8
+ * size — sm, default, lg
9
+ * design — flat, soft, 3d, glass, neo, brutal, gradient
10
+ *
11
+ * (`tone` is accepted as a deprecated alias of `variant`.)
12
+ */
13
+
14
+ export type AlertVariant =
15
+ | "default"
16
+ | "destructive"
17
+ | "success"
18
+ | "warning"
19
+ | "info"
20
+ | "outline"
21
+ | "secondary";
22
+ export type AlertSize = "default" | "sm" | "lg";
23
+ export type AlertDesign = "flat" | "soft" | "3d" | "glass" | "neo" | "brutal" | "gradient";
24
+
25
+ const VARIANTS: Record<AlertVariant, string> = {
26
+ default: "border-border bg-card text-card-foreground",
27
+ destructive:
28
+ "border-destructive/30 bg-destructive/10 text-destructive [&_[data-alert-description]]:text-destructive/90",
29
+ success:
30
+ "border-emerald-600/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 " +
31
+ "[&_[data-alert-description]]:text-emerald-700/90 dark:[&_[data-alert-description]]:text-emerald-400/90",
32
+ warning:
33
+ "border-amber-600/30 bg-amber-500/10 text-amber-700 dark:text-amber-400 " +
34
+ "[&_[data-alert-description]]:text-amber-700/90 dark:[&_[data-alert-description]]:text-amber-400/90",
35
+ info:
36
+ "border-sky-600/30 bg-sky-500/10 text-sky-700 dark:text-sky-400 " +
37
+ "[&_[data-alert-description]]:text-sky-700/90 dark:[&_[data-alert-description]]:text-sky-400/90",
38
+ outline: "border-2 border-border bg-transparent text-foreground",
39
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
40
+ };
41
+
42
+ const SIZES: Record<AlertSize, string> = {
43
+ sm: "p-3 text-xs [&_[data-alert-title]]:text-sm",
44
+ default: "p-4 text-sm",
45
+ lg: "p-6 text-base",
46
+ };
47
+
48
+ const DESIGNS: Record<AlertDesign, string> = {
49
+ flat: "",
50
+ soft: "rounded-2xl border-transparent",
51
+ // Depth via a darker bottom lip + a top sheen — no drop shadow.
52
+ "3d": "border-b-4 border-b-black/15 bg-linear-to-b from-white/20 to-transparent dark:border-b-black/40",
53
+ // Liquid glass panel.
54
+ glass:
55
+ "border-white/40 bg-white/15 backdrop-blur-xl backdrop-saturate-200 " +
56
+ "bg-linear-to-br from-white/30 via-white/10 to-white/5 " +
57
+ "shadow-[inset_0_1px_1px_rgba(255,255,255,0.55),0_6px_24px_rgba(31,38,135,0.16)] " +
58
+ "dark:border-white/20 dark:bg-white/10 dark:from-white/15 dark:via-white/5 dark:to-transparent " +
59
+ "dark:shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_6px_24px_rgba(0,0,0,0.4)]",
60
+ neo:
61
+ "border-transparent bg-background " +
62
+ "shadow-[6px_6px_12px_rgba(0,0,0,0.12),-6px_-6px_12px_rgba(255,255,255,0.8)] " +
63
+ "dark:shadow-[6px_6px_12px_rgba(0,0,0,0.6),-6px_-6px_12px_rgba(255,255,255,0.05)]",
64
+ brutal: "rounded-none border-2 border-foreground shadow-[4px_4px_0_0_var(--color-foreground)]",
65
+ gradient:
66
+ "border-2 border-transparent " +
67
+ "[background:linear-gradient(var(--color-card),var(--color-card))_padding-box," +
68
+ "linear-gradient(135deg,#8b5cf6,#d946ef,#fb923c)_border-box]",
69
+ };
70
+
71
+ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
72
+ variant?: AlertVariant;
73
+ /** @deprecated use `variant` */
74
+ tone?: AlertVariant;
75
+ size?: AlertSize;
76
+ design?: AlertDesign;
77
+ }
78
+
79
+ export function Alert({
80
+ className,
81
+ variant,
82
+ tone,
83
+ size = "default",
84
+ design = "flat",
85
+ ...props
86
+ }: AlertProps) {
87
+ const v = variant ?? tone ?? "default";
88
+ return (
89
+ <div
90
+ role="alert"
91
+ className={cn(
92
+ "relative grid w-full grid-cols-[0_1fr] gap-y-0.5 rounded-lg border",
93
+ "has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5",
94
+ // Reserve room on the trailing edge when an AlertAction is present.
95
+ "has-[[data-alert-action]]:pr-16",
96
+ VARIANTS[v],
97
+ SIZES[size],
98
+ DESIGNS[design],
99
+ className,
100
+ )}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ export function AlertTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
107
+ return (
108
+ <h5
109
+ data-alert-title
110
+ className={cn("col-start-2 mb-1 font-medium leading-none tracking-tight", className)}
111
+ {...props}
112
+ />
113
+ );
114
+ }
115
+
116
+ export function AlertDescription({
117
+ className,
118
+ ...props
119
+ }: React.HTMLAttributes<HTMLParagraphElement>) {
120
+ return (
121
+ <div
122
+ data-alert-description
123
+ className={cn("col-start-2 text-muted-foreground [&_p]:leading-relaxed", className)}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ /** A trailing action (button, link, …) pinned to the top-right of the alert. */
130
+ export function AlertAction({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
131
+ return (
132
+ <div data-alert-action className={cn("absolute right-3 top-3", className)} {...props} />
133
+ );
134
+ }
@@ -0,0 +1,89 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
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) => (
47
+ <div
48
+ ref={ref}
49
+ className={cn(
50
+ "relative flex shrink-0 overflow-hidden rounded-full",
51
+ SIZES[size],
52
+ DESIGNS[design],
53
+ className,
54
+ )}
55
+ {...props}
56
+ />
57
+ ),
58
+ );
59
+ Avatar.displayName = "Avatar";
60
+
61
+ const AvatarImage = React.forwardRef<HTMLImageElement, React.ImgHTMLAttributes<HTMLImageElement>>(
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
+ />
70
+ ),
71
+ );
72
+ AvatarImage.displayName = "AvatarImage";
73
+
74
+ const AvatarFallback = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
75
+ ({ className, ...props }, ref) => (
76
+ <div
77
+ ref={ref}
78
+ data-avatar-fallback
79
+ className={cn(
80
+ "flex size-full items-center justify-center rounded-full bg-secondary font-medium text-secondary-foreground",
81
+ className,
82
+ )}
83
+ {...props}
84
+ />
85
+ ),
86
+ );
87
+ AvatarFallback.displayName = "AvatarFallback";
88
+
89
+ export { Avatar, AvatarImage, AvatarFallback };
@@ -0,0 +1,133 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
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";
29
+
30
+ const VARIANTS: Record<ButtonVariant, string> = {
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",
37
+ };
38
+
39
+ const SIZES: Record<ButtonSize, string> = {
40
+ default: "h-9 px-4 text-sm",
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",
44
+ lg: "h-10 px-6 text-sm",
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",
87
+ };
88
+
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 {
92
+ variant?: ButtonVariant;
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 {
118
+ asChild?: boolean;
119
+ }
120
+
121
+ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
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
+ },
132
+ );
133
+ Button.displayName = "Button";
@@ -0,0 +1,124 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
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) => (
62
+ <div
63
+ ref={ref}
64
+ className={cn(
65
+ "flex flex-col rounded-xl",
66
+ VARIANTS[variant],
67
+ SIZES[size],
68
+ DESIGNS[design],
69
+ className,
70
+ )}
71
+ {...props}
72
+ />
73
+ ),
74
+ );
75
+ Card.displayName = "Card";
76
+
77
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
78
+ ({ className, ...props }, ref) => (
79
+ <div ref={ref} className={cn("flex flex-col gap-1.5 p-(--card-p)", className)} {...props} />
80
+ ),
81
+ );
82
+ CardHeader.displayName = "CardHeader";
83
+
84
+ const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
85
+ ({ className, ...props }, ref) => (
86
+ <h3
87
+ ref={ref}
88
+ className={cn("text-lg font-semibold leading-none tracking-tight", className)}
89
+ {...props}
90
+ />
91
+ ),
92
+ );
93
+ CardTitle.displayName = "CardTitle";
94
+
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>>(
104
+ ({ className, ...props }, ref) => (
105
+ <div ref={ref} className={cn("ml-auto self-start", className)} {...props} />
106
+ ),
107
+ );
108
+ CardAction.displayName = "CardAction";
109
+
110
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
111
+ ({ className, ...props }, ref) => (
112
+ <div ref={ref} className={cn("p-(--card-p) pt-0", className)} {...props} />
113
+ ),
114
+ );
115
+ CardContent.displayName = "CardContent";
116
+
117
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
118
+ ({ className, ...props }, ref) => (
119
+ <div ref={ref} className={cn("flex items-center p-(--card-p) pt-0", className)} {...props} />
120
+ ),
121
+ );
122
+ CardFooter.displayName = "CardFooter";
123
+
124
+ export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };