strade-stx 1.0.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 (129) hide show
  1. package/.activity_counter +1 -0
  2. package/.gitattributes +3 -0
  3. package/.vscode/settings.json +4 -0
  4. package/.vscode/tasks.json +19 -0
  5. package/CHANGELOG.md +1 -0
  6. package/Clarinet.toml +56 -0
  7. package/Clarinet.toml.backup +174 -0
  8. package/Clarinet.toml.old +146 -0
  9. package/DEPLOYMENT_RESULTS.md +160 -0
  10. package/README.md +344 -0
  11. package/TODO.md +34 -0
  12. package/contracts/CoreMarketPlace.clar +227 -0
  13. package/contracts/DisputeResolution_clar.clar +265 -0
  14. package/contracts/EscrowService.clar +171 -0
  15. package/contracts/UserProfile.clar +280 -0
  16. package/contracts/ft-trait.clar +24 -0
  17. package/contracts/token.clar +178 -0
  18. package/costs-reports.json +76026 -0
  19. package/deployments/default.mainnet-plan.yaml +67 -0
  20. package/deployments/default.simnet-plan.yaml +105 -0
  21. package/deployments/default.testnet-plan.yaml +67 -0
  22. package/deployments/new-contracts.testnet-plan.yaml +32 -0
  23. package/frontend/README.md +10 -0
  24. package/frontend/components.json +22 -0
  25. package/frontend/dist/assets/index-BacuuL66.css +1 -0
  26. package/frontend/dist/assets/index-jryypd5B.js +194 -0
  27. package/frontend/dist/favicon.png +0 -0
  28. package/frontend/dist/index.html +15 -0
  29. package/frontend/dist/manifest.json +15 -0
  30. package/frontend/dist/vite.svg +1 -0
  31. package/frontend/empty-mock.js +1 -0
  32. package/frontend/eslint.config.js +23 -0
  33. package/frontend/eslint.config.mjs +25 -0
  34. package/frontend/index.html +14 -0
  35. package/frontend/next.config.ts +17 -0
  36. package/frontend/package-lock.json +14740 -0
  37. package/frontend/package.json +56 -0
  38. package/frontend/postcss.config.js +5 -0
  39. package/frontend/postcss.config.mjs +5 -0
  40. package/frontend/public/favicon.png +0 -0
  41. package/frontend/public/file.svg +1 -0
  42. package/frontend/public/globe.svg +1 -0
  43. package/frontend/public/manifest.json +15 -0
  44. package/frontend/public/next.svg +1 -0
  45. package/frontend/public/vercel.svg +1 -0
  46. package/frontend/public/vite.svg +1 -0
  47. package/frontend/public/window.svg +1 -0
  48. package/frontend/src/App.css +42 -0
  49. package/frontend/src/App.tsx +177 -0
  50. package/frontend/src/app/about/page.tsx +208 -0
  51. package/frontend/src/app/favicon.ico +0 -0
  52. package/frontend/src/app/globals.css +129 -0
  53. package/frontend/src/app/help/page.tsx +167 -0
  54. package/frontend/src/app/how-it-works/page.tsx +274 -0
  55. package/frontend/src/app/layout.tsx +55 -0
  56. package/frontend/src/app/marketplace/page.tsx +324 -0
  57. package/frontend/src/app/my-listings/page.tsx +318 -0
  58. package/frontend/src/app/page.tsx +15 -0
  59. package/frontend/src/assets/react.svg +1 -0
  60. package/frontend/src/components/ConfirmDialog.tsx +54 -0
  61. package/frontend/src/components/CreateListingForm.tsx +231 -0
  62. package/frontend/src/components/ErrorBoundary.tsx +73 -0
  63. package/frontend/src/components/FilterPanel.tsx +10 -0
  64. package/frontend/src/components/Footer.tsx +100 -0
  65. package/frontend/src/components/Header.tsx +268 -0
  66. package/frontend/src/components/ImageUpload.tsx +147 -0
  67. package/frontend/src/components/LandingPage.tsx +322 -0
  68. package/frontend/src/components/ListingCard.tsx +154 -0
  69. package/frontend/src/components/LoadingSkeleton.tsx +44 -0
  70. package/frontend/src/components/MobileNav.tsx +89 -0
  71. package/frontend/src/components/NotificationBell.tsx +8 -0
  72. package/frontend/src/components/NotificationPanel.tsx +14 -0
  73. package/frontend/src/components/README.md +14 -0
  74. package/frontend/src/components/SearchBar.tsx +10 -0
  75. package/frontend/src/components/TestnetBanner.tsx +29 -0
  76. package/frontend/src/components/ThemeToggle.tsx +32 -0
  77. package/frontend/src/components/__tests__/Header.test.tsx +70 -0
  78. package/frontend/src/components/__tests__/ListingCard.test.tsx +86 -0
  79. package/frontend/src/components/providers/ThemeProvider.tsx +9 -0
  80. package/frontend/src/components/ui/alert-dialog.tsx +141 -0
  81. package/frontend/src/components/ui/avatar.tsx +53 -0
  82. package/frontend/src/components/ui/badge.tsx +46 -0
  83. package/frontend/src/components/ui/button.tsx +60 -0
  84. package/frontend/src/components/ui/card.tsx +92 -0
  85. package/frontend/src/components/ui/dialog.tsx +143 -0
  86. package/frontend/src/components/ui/dropdown-menu.tsx +257 -0
  87. package/frontend/src/components/ui/input.tsx +21 -0
  88. package/frontend/src/components/ui/label.tsx +24 -0
  89. package/frontend/src/components/ui/select.tsx +187 -0
  90. package/frontend/src/components/ui/sonner.tsx +40 -0
  91. package/frontend/src/components/ui/textarea.tsx +18 -0
  92. package/frontend/src/context/README.md +27 -0
  93. package/frontend/src/index.css +166 -0
  94. package/frontend/src/lib/notificationEvents.ts +10 -0
  95. package/frontend/src/lib/notificationStore.ts +13 -0
  96. package/frontend/src/lib/notifications.ts +13 -0
  97. package/frontend/src/lib/search.ts +28 -0
  98. package/frontend/src/lib/stacks.ts +189 -0
  99. package/frontend/src/lib/utils.ts +6 -0
  100. package/frontend/src/main.tsx +10 -0
  101. package/frontend/src/test/setup.ts +23 -0
  102. package/frontend/src/types.d.ts +9 -0
  103. package/frontend/tsconfig.app.json +28 -0
  104. package/frontend/tsconfig.json +41 -0
  105. package/frontend/tsconfig.node.json +26 -0
  106. package/frontend/vercel.json +4 -0
  107. package/frontend/vite.config.ts +6 -0
  108. package/frontend/vitest.config.ts +17 -0
  109. package/lcov.info +31338 -0
  110. package/mainnetcontracts.md +16 -0
  111. package/package.json +53 -0
  112. package/scripts/auto-activity.sh +9 -0
  113. package/scripts/cancel-pending.ts +67 -0
  114. package/scripts/check-balances.ts +23 -0
  115. package/scripts/distribute-evenly.ts +56 -0
  116. package/scripts/drain-accounts.ts +70 -0
  117. package/scripts/fund-accounts.ts +88 -0
  118. package/scripts/fund-active.ts +59 -0
  119. package/scripts/fund-unfunded.ts +88 -0
  120. package/scripts/generate-activity.ts +181 -0
  121. package/scripts/git-activity-generator.ts +154 -0
  122. package/scripts/mobile-server.ts +123 -0
  123. package/settings/Devnet.toml +155 -0
  124. package/settings/Mainnet.toml +7 -0
  125. package/settings/Testnet.toml +9 -0
  126. package/tests/CoreMarketPlace.fuzz.test.ts +435 -0
  127. package/tests/CoreMarketPlace.test.ts +564 -0
  128. package/tsconfig.json +26 -0
  129. package/vitest.config.js +49 -0
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Moon, Sun } from 'lucide-react';
5
+ import { useTheme } from 'next-themes';
6
+ import { Button } from '@/components/ui/button';
7
+
8
+ export function ThemeToggle() {
9
+ const { theme, setTheme } = useTheme();
10
+ const [mounted, setMounted] = React.useState(false);
11
+
12
+ React.useEffect(() => {
13
+ setMounted(true);
14
+ }, []);
15
+
16
+ if (!mounted) {
17
+ return <Button variant="ghost" size="icon" className="w-9 h-9" />;
18
+ }
19
+
20
+ return (
21
+ <Button
22
+ variant="ghost"
23
+ size="icon"
24
+ onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
25
+ className="w-9 h-9"
26
+ >
27
+ <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
28
+ <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
29
+ <span className="sr-only">Toggle theme</span>
30
+ </Button>
31
+ );
32
+ }
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+
4
+ import Header from '../Header';
5
+ import * as navigation from 'next/navigation';
6
+
7
+ // Mock stacks
8
+ vi.mock('@/lib/stacks', () => ({
9
+ userSession: {
10
+ isUserSignedIn: vi.fn(() => false),
11
+ loadUserData: vi.fn(() => ({
12
+ profile: {
13
+ stxAddress: {
14
+ testnet: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
15
+ },
16
+ },
17
+ })),
18
+ signUserOut: vi.fn(),
19
+ },
20
+ formatAddress: vi.fn((address: string) => `${address.slice(0, 6)}...${address.slice(-4)}`),
21
+ }));
22
+
23
+ describe('Header Component', () => {
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ });
27
+
28
+ it('renders the header with logo and navigation', () => {
29
+ render(<Header />);
30
+
31
+ expect(screen.getByText('Strade')).toBeInTheDocument();
32
+ expect(screen.getByText('Decentralized Marketplace')).toBeInTheDocument();
33
+ expect(screen.getByText('Marketplace')).toBeInTheDocument();
34
+ expect(screen.getByText('My Listings')).toBeInTheDocument();
35
+ });
36
+
37
+ it('shows Connect Wallet button when not connected', () => {
38
+ render(<Header />);
39
+
40
+ expect(screen.getByText('Connect Wallet')).toBeInTheDocument();
41
+ });
42
+
43
+ it('renders navigation links', () => {
44
+ render(<Header />);
45
+
46
+ const marketplaceLink = screen.getByText('Marketplace').closest('a');
47
+ const myListingsLink = screen.getByText('My Listings').closest('a');
48
+
49
+ expect(marketplaceLink).toHaveAttribute('href', '/');
50
+ expect(myListingsLink).toHaveAttribute('href', '/my-listings');
51
+ });
52
+
53
+ it('applies active styling to current page', () => {
54
+ vi.spyOn(navigation, 'usePathname').mockReturnValue('/');
55
+
56
+ render(<Header />);
57
+
58
+ const marketplaceSpan = screen.getByText('Marketplace');
59
+ expect(marketplaceSpan).toHaveClass('text-slate-900', 'bg-slate-100');
60
+ });
61
+
62
+ it('applies hover styling to non-active links', () => {
63
+ vi.spyOn(navigation, 'usePathname').mockReturnValue('/');
64
+
65
+ render(<Header />);
66
+
67
+ const myListingsSpan = screen.getByText('My Listings');
68
+ expect(myListingsSpan).toHaveClass('text-slate-600', 'hover:text-slate-900');
69
+ });
70
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import ListingCard from '../ListingCard';
4
+ import { Listing } from '@/lib/stacks';
5
+
6
+ const mockListing: Listing = {
7
+ listingId: 1,
8
+ seller: 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM',
9
+ name: 'Test Item',
10
+ description: 'A test item for sale',
11
+ price: 1000000,
12
+ status: 'active',
13
+ createdAt: 1000,
14
+ expiresAt: Math.floor(Date.now() / 1000) + 86400, // 1 day from now
15
+ };
16
+
17
+ describe('ListingCard Component', () => {
18
+ it('renders listing information correctly', () => {
19
+ render(<ListingCard listing={mockListing} />);
20
+
21
+ expect(screen.getByText('Test Item')).toBeInTheDocument();
22
+ expect(screen.getByText('A test item for sale')).toBeInTheDocument();
23
+ expect(screen.getByText('1.000000 STX')).toBeInTheDocument();
24
+ expect(screen.getByText('active')).toBeInTheDocument();
25
+ });
26
+
27
+ it('shows Purchase button for non-owner active listings', () => {
28
+ render(<ListingCard listing={mockListing} isOwner={false} />);
29
+
30
+ expect(screen.getByText('Purchase')).toBeInTheDocument();
31
+ });
32
+
33
+ it('shows "Your listing" text for owner', () => {
34
+ render(<ListingCard listing={mockListing} isOwner={true} />);
35
+
36
+ expect(screen.getByText('Your listing')).toBeInTheDocument();
37
+ expect(screen.queryByText('Purchase')).not.toBeInTheDocument();
38
+ });
39
+
40
+ it('calls onPurchase when Purchase button is clicked', async () => {
41
+ const onPurchase = vi.fn();
42
+ render(<ListingCard listing={mockListing} onPurchase={onPurchase} isOwner={false} />);
43
+
44
+ const purchaseButton = screen.getByText('Purchase');
45
+ fireEvent.click(purchaseButton);
46
+
47
+ expect(onPurchase).toHaveBeenCalledWith(1);
48
+ });
49
+
50
+ it('shows expired badge for expired listings', () => {
51
+ const expiredListing = {
52
+ ...mockListing,
53
+ expiresAt: Math.floor(Date.now() / 1000) - 86400, // 1 day ago
54
+ };
55
+
56
+ render(<ListingCard listing={expiredListing} />);
57
+
58
+ expect(screen.getByText('Expired')).toBeInTheDocument();
59
+ });
60
+
61
+ it('shows "Not available" for sold listings', () => {
62
+ const soldListing = {
63
+ ...mockListing,
64
+ status: 'sold',
65
+ };
66
+
67
+ render(<ListingCard listing={soldListing} isOwner={false} />);
68
+
69
+ expect(screen.getByText('Not available')).toBeInTheDocument();
70
+ expect(screen.queryByText('Purchase')).not.toBeInTheDocument();
71
+ });
72
+
73
+ it('formats seller address correctly', () => {
74
+ render(<ListingCard listing={mockListing} />);
75
+
76
+ // The actual formatted address from formatAddress function
77
+ expect(screen.getByText('ST1PQH...GZGM')).toBeInTheDocument();
78
+ });
79
+
80
+ it('displays expiration date', () => {
81
+ render(<ListingCard listing={mockListing} />);
82
+
83
+ const expirationText = screen.getByText(/Expires:/);
84
+ expect(expirationText).toBeInTheDocument();
85
+ });
86
+ });
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { ThemeProvider as NextThemesProvider } from 'next-themes';
5
+ import { type ThemeProviderProps } from 'next-themes';
6
+
7
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
9
+ }
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
5
+
6
+ import { cn } from '@/lib/utils';
7
+ import { buttonVariants } from '@/components/ui/button';
8
+
9
+ const AlertDialog = AlertDialogPrimitive.Root;
10
+
11
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
12
+
13
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
14
+
15
+ const AlertDialogOverlay = React.forwardRef<
16
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
17
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
18
+ >(({ className, ...props }, ref) => (
19
+ <AlertDialogPrimitive.Overlay
20
+ className={cn(
21
+ 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
22
+ className
23
+ )}
24
+ {...props}
25
+ ref={ref}
26
+ />
27
+ ));
28
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
29
+
30
+ const AlertDialogContent = React.forwardRef<
31
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
32
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
33
+ >(({ className, ...props }, ref) => (
34
+ <AlertDialogPortal>
35
+ <AlertDialogOverlay />
36
+ <AlertDialogPrimitive.Content
37
+ ref={ref}
38
+ className={cn(
39
+ 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ </AlertDialogPortal>
45
+ ));
46
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
47
+
48
+ const AlertDialogHeader = ({
49
+ className,
50
+ ...props
51
+ }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div
53
+ className={cn(
54
+ 'flex flex-col space-y-2 text-center sm:text-left',
55
+ className
56
+ )}
57
+ {...props}
58
+ />
59
+ );
60
+ AlertDialogHeader.displayName = 'AlertDialogHeader';
61
+
62
+ const AlertDialogFooter = ({
63
+ className,
64
+ ...props
65
+ }: React.HTMLAttributes<HTMLDivElement>) => (
66
+ <div
67
+ className={cn(
68
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
69
+ className
70
+ )}
71
+ {...props}
72
+ />
73
+ );
74
+ AlertDialogFooter.displayName = 'AlertDialogFooter';
75
+
76
+ const AlertDialogTitle = React.forwardRef<
77
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
78
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
79
+ >(({ className, ...props }, ref) => (
80
+ <AlertDialogPrimitive.Title
81
+ ref={ref}
82
+ className={cn('text-lg font-semibold', className)}
83
+ {...props}
84
+ />
85
+ ));
86
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
87
+
88
+ const AlertDialogDescription = React.forwardRef<
89
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
90
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
91
+ >(({ className, ...props }, ref) => (
92
+ <AlertDialogPrimitive.Description
93
+ ref={ref}
94
+ className={cn('text-sm text-slate-500', className)}
95
+ {...props}
96
+ />
97
+ ));
98
+ AlertDialogDescription.displayName =
99
+ AlertDialogPrimitive.Description.displayName;
100
+
101
+ const AlertDialogAction = React.forwardRef<
102
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
103
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
104
+ >(({ className, ...props }, ref) => (
105
+ <AlertDialogPrimitive.Action
106
+ ref={ref}
107
+ className={cn(buttonVariants(), className)}
108
+ {...props}
109
+ />
110
+ ));
111
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
112
+
113
+ const AlertDialogCancel = React.forwardRef<
114
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
115
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
116
+ >(({ className, ...props }, ref) => (
117
+ <AlertDialogPrimitive.Cancel
118
+ ref={ref}
119
+ className={cn(
120
+ buttonVariants({ variant: 'outline' }),
121
+ 'mt-2 sm:mt-0',
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ ));
127
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
128
+
129
+ export {
130
+ AlertDialog,
131
+ AlertDialogPortal,
132
+ AlertDialogOverlay,
133
+ AlertDialogTrigger,
134
+ AlertDialogContent,
135
+ AlertDialogHeader,
136
+ AlertDialogFooter,
137
+ AlertDialogTitle,
138
+ AlertDialogDescription,
139
+ AlertDialogAction,
140
+ AlertDialogCancel,
141
+ };
@@ -0,0 +1,53 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback }
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }
@@ -0,0 +1,60 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ }
37
+ )
38
+
39
+ function Button({
40
+ className,
41
+ variant,
42
+ size,
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean
48
+ }) {
49
+ const Comp = asChild ? Slot : "button"
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+
60
+ export { Button, buttonVariants }
@@ -0,0 +1,92 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }