@swift-rust/ui 0.2.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +89 -41
- package/dist/cli.js.map +1 -1
- package/dist/cli.test.d.ts +2 -0
- package/dist/cli.test.d.ts.map +1 -0
- package/dist/cli.test.js +36 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +61 -32
- package/dist/components.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/registry.test.d.ts +2 -0
- package/dist/registry.test.d.ts.map +1 -0
- package/dist/registry.test.js +82 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/smoke.test.js +5 -3
- package/dist/smoke.test.js.map +1 -1
- package/package.json +7 -7
- package/registry/components/accordion.tsx +125 -16
- package/registry/components/alert-dialog.tsx +102 -0
- package/registry/components/alert.tsx +114 -14
- package/registry/components/aspect-ratio.tsx +18 -0
- package/registry/components/avatar.tsx +59 -7
- package/registry/components/badge.tsx +29 -14
- package/registry/components/breadcrumb.tsx +7 -13
- package/registry/components/button-group.tsx +28 -0
- package/registry/components/button.tsx +113 -28
- package/registry/components/calendar.tsx +92 -0
- package/registry/components/callout.tsx +14 -14
- package/registry/components/card.tsx +87 -12
- package/registry/components/carousel.tsx +41 -0
- package/registry/components/chart.tsx +50 -0
- package/registry/components/checkbox.tsx +5 -5
- package/registry/components/code-block.tsx +118 -0
- package/registry/components/code.tsx +2 -3
- package/registry/components/collapsible.tsx +60 -0
- package/registry/components/combobox.tsx +102 -0
- package/registry/components/command.tsx +5 -5
- package/registry/components/context-menu.tsx +81 -0
- package/registry/components/data-table.tsx +71 -0
- package/registry/components/date-picker.tsx +58 -0
- package/registry/components/dialog.tsx +2 -2
- package/registry/components/direction.tsx +17 -0
- package/registry/components/drawer.tsx +77 -0
- package/registry/components/dropdown-menu.tsx +5 -5
- package/registry/components/empty.tsx +34 -0
- package/registry/components/field.tsx +27 -0
- package/registry/components/file-upload.tsx +116 -0
- package/registry/components/form.tsx +3 -4
- package/registry/components/hover-card.tsx +59 -0
- package/registry/components/input-group.tsx +34 -0
- package/registry/components/input-otp.tsx +50 -0
- package/registry/components/input.tsx +71 -7
- package/registry/components/item.tsx +42 -0
- package/registry/components/kbd.tsx +3 -4
- package/registry/components/label.tsx +34 -4
- package/registry/components/menubar.tsx +60 -0
- package/registry/components/native-select.tsx +35 -0
- package/registry/components/navigation-menu.tsx +3 -3
- package/registry/components/pagination.tsx +4 -5
- package/registry/components/popover.tsx +1 -1
- package/registry/components/progress.tsx +10 -5
- package/registry/components/radio-group.tsx +9 -9
- package/registry/components/resizable.tsx +77 -0
- package/registry/components/scroll-area.tsx +20 -0
- package/registry/components/select.tsx +2 -3
- package/registry/components/separator.tsx +1 -2
- package/registry/components/sheet.tsx +1 -1
- package/registry/components/sidebar.tsx +72 -0
- package/registry/components/skeleton.tsx +1 -6
- package/registry/components/slider.tsx +6 -3
- package/registry/components/sonner.tsx +52 -0
- package/registry/components/spinner.tsx +19 -6
- package/registry/components/stepper.tsx +63 -0
- package/registry/components/switch.tsx +7 -6
- package/registry/components/table.tsx +2 -3
- package/registry/components/tabs.tsx +3 -3
- package/registry/components/textarea.tsx +42 -6
- package/registry/components/toast.tsx +2 -2
- package/registry/components/toggle-group.tsx +72 -0
- package/registry/components/toggle.tsx +45 -20
- 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
|
-
|
|
6
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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<
|
|
16
|
-
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
|
-
<
|
|
35
|
+
<span
|
|
22
36
|
className={cn(
|
|
23
|
-
"inline-flex items-center gap-1 rounded-full border
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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-
|
|
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
|
-
|
|
6
|
-
|
|
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-
|
|
10
|
-
destructive: "bg-
|
|
11
|
-
outline: "border border-
|
|
12
|
-
secondary: "bg-
|
|
13
|
-
ghost: "hover:bg-
|
|
14
|
-
link: "text-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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(
|
|
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
|
|
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
|
|
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<
|
|
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
|
-
<
|
|
105
|
+
<div ref={ref} className={cn("ml-auto self-start", className)} {...props} />
|
|
33
106
|
),
|
|
34
107
|
);
|
|
35
|
-
|
|
108
|
+
CardAction.displayName = "CardAction";
|
|
36
109
|
|
|
37
110
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
38
|
-
({ className, ...props }, ref) =>
|
|
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-
|
|
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 };
|