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.
- package/dist/index.js +204 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/package.json +5 -5
- package/templates/ui/accordion.tsx +215 -0
- package/templates/ui/alert.tsx +134 -0
- package/templates/ui/avatar.tsx +89 -0
- package/templates/ui/button.tsx +133 -0
- package/templates/ui/card.tsx +124 -0
- package/templates/ui/input.tsx +87 -0
- package/templates/ui/label.tsx +49 -0
- package/templates/ui/utils.ts +6 -0
|
@@ -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 };
|