@amirulabu/create-recurring-rabbit-app 0.0.0-alpha

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 (64) hide show
  1. package/bin/index.js +2 -0
  2. package/dist/index.js +592 -0
  3. package/package.json +43 -0
  4. package/templates/default/.editorconfig +21 -0
  5. package/templates/default/.env.example +15 -0
  6. package/templates/default/.eslintrc.json +35 -0
  7. package/templates/default/.prettierrc.json +7 -0
  8. package/templates/default/README.md +346 -0
  9. package/templates/default/app.config.ts +20 -0
  10. package/templates/default/docs/adding-features.md +439 -0
  11. package/templates/default/docs/adr/001-use-sqlite-for-development-database.md +22 -0
  12. package/templates/default/docs/adr/002-use-tanstack-start-over-nextjs.md +22 -0
  13. package/templates/default/docs/adr/003-use-better-auth-over-nextauth.md +22 -0
  14. package/templates/default/docs/adr/004-use-drizzle-over-prisma.md +22 -0
  15. package/templates/default/docs/adr/005-use-trpc-for-api-layer.md +22 -0
  16. package/templates/default/docs/adr/006-use-tailwind-css-v4-with-shadcn-ui.md +22 -0
  17. package/templates/default/docs/architecture.md +241 -0
  18. package/templates/default/docs/database.md +376 -0
  19. package/templates/default/docs/deployment.md +435 -0
  20. package/templates/default/docs/troubleshooting.md +668 -0
  21. package/templates/default/drizzle/migrations/0001_initial_schema.sql +39 -0
  22. package/templates/default/drizzle/migrations/meta/0001_snapshot.json +225 -0
  23. package/templates/default/drizzle/migrations/meta/_journal.json +12 -0
  24. package/templates/default/drizzle.config.ts +10 -0
  25. package/templates/default/lighthouserc.json +78 -0
  26. package/templates/default/src/app/__root.tsx +32 -0
  27. package/templates/default/src/app/api/auth/$.ts +15 -0
  28. package/templates/default/src/app/api/trpc.server.ts +12 -0
  29. package/templates/default/src/app/auth/forgot-password.tsx +107 -0
  30. package/templates/default/src/app/auth/login.tsx +34 -0
  31. package/templates/default/src/app/auth/register.tsx +34 -0
  32. package/templates/default/src/app/auth/reset-password.tsx +171 -0
  33. package/templates/default/src/app/auth/verify-email.tsx +111 -0
  34. package/templates/default/src/app/dashboard/index.tsx +122 -0
  35. package/templates/default/src/app/dashboard/settings.tsx +161 -0
  36. package/templates/default/src/app/globals.css +55 -0
  37. package/templates/default/src/app/index.tsx +83 -0
  38. package/templates/default/src/components/features/auth/login-form.tsx +172 -0
  39. package/templates/default/src/components/features/auth/register-form.tsx +202 -0
  40. package/templates/default/src/components/layout/dashboard-layout.tsx +27 -0
  41. package/templates/default/src/components/layout/header.tsx +29 -0
  42. package/templates/default/src/components/layout/sidebar.tsx +38 -0
  43. package/templates/default/src/components/ui/button.tsx +57 -0
  44. package/templates/default/src/components/ui/card.tsx +79 -0
  45. package/templates/default/src/components/ui/input.tsx +24 -0
  46. package/templates/default/src/lib/api.ts +42 -0
  47. package/templates/default/src/lib/auth.ts +292 -0
  48. package/templates/default/src/lib/email.ts +221 -0
  49. package/templates/default/src/lib/env.ts +119 -0
  50. package/templates/default/src/lib/hydration-timing.ts +289 -0
  51. package/templates/default/src/lib/monitoring.ts +336 -0
  52. package/templates/default/src/lib/utils.ts +6 -0
  53. package/templates/default/src/server/api/root.ts +10 -0
  54. package/templates/default/src/server/api/routers/dashboard.ts +37 -0
  55. package/templates/default/src/server/api/routers/user.ts +31 -0
  56. package/templates/default/src/server/api/trpc.ts +132 -0
  57. package/templates/default/src/server/auth/config.ts +241 -0
  58. package/templates/default/src/server/db/index.ts +153 -0
  59. package/templates/default/src/server/db/migrate.ts +125 -0
  60. package/templates/default/src/server/db/schema.ts +170 -0
  61. package/templates/default/src/server/db/seed.ts +130 -0
  62. package/templates/default/src/types/global.d.ts +25 -0
  63. package/templates/default/tailwind.config.js +46 -0
  64. package/templates/default/tsconfig.json +36 -0
@@ -0,0 +1,83 @@
1
+ import { createFileRoute, Link } from '@tanstack/react-router'
2
+
3
+ export const Route = createFileRoute()({
4
+ component: Index,
5
+ })
6
+
7
+ function Index() {
8
+ return (
9
+ <div className="min-h-screen bg-gradient-to-b from-blue-50 to-white">
10
+ <div className="container mx-auto px-4 py-16">
11
+ <nav className="flex justify-between items-center mb-16">
12
+ <h1 className="text-2xl font-bold text-blue-600">Recurring Rabbit</h1>
13
+ <div className="flex gap-4">
14
+ <Link
15
+ from="/"
16
+ to="/auth/login"
17
+ className="px-4 py-2 text-gray-700 hover:text-blue-600 transition-colors"
18
+ >
19
+ Sign In
20
+ </Link>
21
+ <Link
22
+ from="/"
23
+ to="/auth/register"
24
+ className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
25
+ >
26
+ Get Started
27
+ </Link>
28
+ </div>
29
+ </nav>
30
+
31
+ <div className="max-w-3xl mx-auto text-center">
32
+ <h2 className="text-5xl font-bold text-gray-900 mb-6">
33
+ Start your SaaS business in minutes
34
+ </h2>
35
+ <p className="text-xl text-gray-600 mb-8">
36
+ A production-ready starter with authentication, database, and all
37
+ the tools you need to ship fast.
38
+ </p>
39
+ <div className="flex justify-center gap-4">
40
+ <Link
41
+ from="/"
42
+ to="/auth/register"
43
+ className="px-6 py-3 bg-blue-600 text-white rounded-lg text-lg font-medium hover:bg-blue-700 transition-colors"
44
+ >
45
+ Start Free Trial
46
+ </Link>
47
+ <Link
48
+ from="/"
49
+ to="/auth/login"
50
+ className="px-4 py-2 text-gray-700 hover:text-blue-600 transition-colors"
51
+ >
52
+ Sign In
53
+ </Link>
54
+ </div>
55
+ </div>
56
+
57
+ <div className="mt-20 grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
58
+ <div className="p-6 bg-white rounded-lg shadow-md">
59
+ <h3 className="text-xl font-semibold mb-3">Authentication</h3>
60
+ <p className="text-gray-600">
61
+ Email/password authentication with verification, session
62
+ management, and secure defaults.
63
+ </p>
64
+ </div>
65
+ <div className="p-6 bg-white rounded-lg shadow-md">
66
+ <h3 className="text-xl font-semibold mb-3">Database</h3>
67
+ <p className="text-gray-600">
68
+ Type-safe database with Drizzle ORM, SQLite for dev, and
69
+ PostgreSQL for production.
70
+ </p>
71
+ </div>
72
+ <div className="p-6 bg-white rounded-lg shadow-md">
73
+ <h3 className="text-xl font-semibold mb-3">Type Safety</h3>
74
+ <p className="text-gray-600">
75
+ End-to-end type safety from database schema to UI components with
76
+ TypeScript.
77
+ </p>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,172 @@
1
+ import { useState } from 'react'
2
+ import { authClient } from '@/lib/auth'
3
+ import { useRouter, useSearch } from '@tanstack/react-router'
4
+ import { Button } from '@/components/ui/button'
5
+ import { Input } from '@/components/ui/input'
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from '@/components/ui/card'
13
+
14
+ /**
15
+ * LoginForm Component
16
+ *
17
+ * Provides a user interface for authenticating existing users via email and password.
18
+ * This is the entry point for the authentication flow, allowing users to sign in to access
19
+ * protected resources and dashboard features.
20
+ *
21
+ * Authentication Flow:
22
+ * 1. User enters credentials (email and password)
23
+ * 2. Form validates HTML5 constraints (required fields, email format)
24
+ * 3. Credentials are sent to the auth service via signInEmail
25
+ * 4. On success, user is redirected to /dashboard
26
+ * 5. On failure, user-friendly error message is displayed
27
+ *
28
+ * Key Features:
29
+ * - Client-side form validation using HTML5 built-in validation
30
+ * - Loading state management to prevent double submissions
31
+ * - Clear error messaging with visual feedback
32
+ * - Automatic redirect on successful authentication
33
+ * - Form controls are disabled during submission to prevent user interaction
34
+ *
35
+ * UX Considerations:
36
+ * - Centered card layout with max-width for optimal readability
37
+ * - Password masking for security
38
+ * - Error messages displayed inline near the form for better visibility
39
+ * - Loading state shown in button text to communicate progress
40
+ * - Form reset not needed since redirect occurs on success
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * import { LoginForm } from '@/components/features/auth/login-form'
45
+ *
46
+ * function LoginPage() {
47
+ * return <LoginForm />
48
+ * }
49
+ * ```
50
+ */
51
+ export function LoginForm() {
52
+ const router = useRouter()
53
+ const search = useSearch({ from: '/auth/login' })
54
+ const redirect = (search.redirect as string) || '/dashboard'
55
+ const [loading, setLoading] = useState(false)
56
+ const [error, setError] = useState('')
57
+
58
+ /**
59
+ * Handles form submission for user authentication.
60
+ *
61
+ * Extracts email and password from the form, validates credentials through the auth service,
62
+ * and manages the user flow based on authentication result.
63
+ *
64
+ * Form Validation:
65
+ * - Email: Uses HTML5 type="email" for basic format validation
66
+ * - Password: Uses HTML5 required attribute
67
+ * - Additional validation happens server-side via auth service
68
+ *
69
+ * @param e - React form event from the form submission
70
+ * @returns Promise<void> - Does not return a value
71
+ *
72
+ * Side Effects:
73
+ * - Sets loading state to true during authentication, false after completion
74
+ * - Clears previous error state before attempting authentication
75
+ * - On success: Navigates to the redirect URL (from search params) or /dashboard
76
+ * - On failure: Sets error state with user-friendly message
77
+ *
78
+ * Error Handling:
79
+ * - API errors from auth service display the error message
80
+ * - Network/unexpected errors show generic "Invalid email or password"
81
+ * - Errors persist until user tries again or leaves page
82
+ */
83
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
84
+ e.preventDefault()
85
+ setLoading(true)
86
+ setError('')
87
+
88
+ const formData = new FormData(e.currentTarget)
89
+ const email = formData.get('email') as string
90
+ const password = formData.get('password') as string
91
+
92
+ try {
93
+ const result = await authClient.signIn.email({
94
+ email,
95
+ password,
96
+ })
97
+
98
+ if (result.error) {
99
+ setError(result.error.message || 'Invalid email or password')
100
+ return
101
+ }
102
+
103
+ router.navigate({ to: redirect })
104
+ } catch (err) {
105
+ setError('Invalid email or password')
106
+ } finally {
107
+ setLoading(false)
108
+ }
109
+ }
110
+
111
+ return (
112
+ <Card className="w-full max-w-md mx-auto">
113
+ <CardHeader>
114
+ <CardTitle>Welcome back</CardTitle>
115
+ <CardDescription>Sign in to your account to continue</CardDescription>
116
+ </CardHeader>
117
+ <CardContent>
118
+ <form onSubmit={handleSubmit} className="space-y-4">
119
+ <div className="space-y-2">
120
+ <label
121
+ htmlFor="email"
122
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
123
+ >
124
+ Email
125
+ </label>
126
+ <Input
127
+ id="email"
128
+ name="email"
129
+ type="email"
130
+ placeholder="john@example.com"
131
+ required
132
+ disabled={loading}
133
+ />
134
+ </div>
135
+ <div className="space-y-2">
136
+ <label
137
+ htmlFor="password"
138
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
139
+ >
140
+ Password
141
+ </label>
142
+ <Input
143
+ id="password"
144
+ name="password"
145
+ type="password"
146
+ placeholder="•••••••"
147
+ required
148
+ disabled={loading}
149
+ />
150
+ </div>
151
+ {error && (
152
+ <div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
153
+ {error}
154
+ </div>
155
+ )}
156
+ <Button type="submit" disabled={loading} className="w-full">
157
+ {loading ? 'Signing in...' : 'Sign in'}
158
+ </Button>
159
+ <div className="text-center">
160
+ <button
161
+ type="button"
162
+ onClick={() => router.navigate({ to: '/auth/forgot-password' })}
163
+ className="text-sm text-gray-600 hover:text-gray-900"
164
+ >
165
+ Forgot your password?
166
+ </button>
167
+ </div>
168
+ </form>
169
+ </CardContent>
170
+ </Card>
171
+ )
172
+ }
@@ -0,0 +1,202 @@
1
+ import { useState } from 'react'
2
+ import { authClient } from '@/lib/auth'
3
+ import { Button } from '@/components/ui/button'
4
+ import { Input } from '@/components/ui/input'
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from '@/components/ui/card'
12
+
13
+ /**
14
+ * RegisterForm Component
15
+ *
16
+ * Provides a user interface for new user registration, collecting essential user information
17
+ * to create an account in the system. This component initiates the user onboarding flow
18
+ * by creating a user account and triggering email verification.
19
+ *
20
+ * Authentication Flow:
21
+ * 1. User enters registration details (name, email, password)
22
+ * 2. Form validates HTML5 constraints (required fields, email format, password length)
23
+ * 3. User data is sent to auth service via signUpEmail
24
+ * 4. On success, verification email is sent and form displays confirmation message
25
+ * 5. User must verify email via link in confirmation email before signing in
26
+ *
27
+ * Key Features:
28
+ * - Client-side form validation with minimum password length requirement (8 characters)
29
+ * - Dual state messaging: success (green) for verification confirmation, error (red) for failures
30
+ * - Form reset after successful registration to allow new attempts
31
+ * - Loading state management to prevent duplicate submissions
32
+ * - Visual feedback using color-coded alerts for different message types
33
+ *
34
+ * UX Considerations:
35
+ * - Centered card layout with max-width for optimal readability
36
+ * - Password masking for security
37
+ * - Password length requirement (minLength={8}) shown to users via browser validation
38
+ * - Success message clearly indicates next steps (check email for verification)
39
+ * - Error messages displayed inline near the form for immediate visibility
40
+ * - Loading state in button text communicates ongoing operation
41
+ * - No automatic redirect - users stay on page to see confirmation message
42
+ *
43
+ * Security Notes:
44
+ * - Password must be at least 8 characters (enforced by HTML5 validation)
45
+ * - Email verification required before user can sign in
46
+ * - All fields are required to prevent incomplete registration
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * import { RegisterForm } from '@/components/features/auth/register-form'
51
+ *
52
+ * function RegisterPage() {
53
+ * return <RegisterForm />
54
+ * }
55
+ * ```
56
+ */
57
+ export function RegisterForm() {
58
+ const [loading, setLoading] = useState(false)
59
+ const [message, setMessage] = useState('')
60
+ const [error, setError] = useState('')
61
+
62
+ /**
63
+ * Handles form submission for new user registration.
64
+ *
65
+ * Extracts user data from the form, validates inputs through the auth service,
66
+ * and manages the registration flow based on the result.
67
+ *
68
+ * Form Validation:
69
+ * - Name: Required field, no format constraints
70
+ * - Email: Uses HTML5 type="email" for basic format validation
71
+ * - Password: Required, minimum 8 characters (enforced via minLength={8})
72
+ * - Additional validation happens server-side via auth service
73
+ *
74
+ * @param e - React form event from the form submission
75
+ * @returns Promise<void> - Does not return a value
76
+ *
77
+ * Side Effects:
78
+ * - Sets loading state to true during registration, false after completion
79
+ * - Clears previous error and message states before attempting registration
80
+ * - On success: Sets success message and resets the form
81
+ * - On failure: Sets error state with user-friendly message
82
+ *
83
+ * Error Handling:
84
+ * - API errors from auth service display the specific error message
85
+ * - Network/unexpected errors show generic "Registration failed" message
86
+ * - Errors persist until user tries again or leaves page
87
+ *
88
+ * Success Behavior:
89
+ * - Displays green confirmation message prompting email verification
90
+ * - Resets form fields to allow another registration attempt
91
+ * - No redirect - user stays on page to see next steps
92
+ */
93
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
94
+ e.preventDefault()
95
+ setLoading(true)
96
+ setError('')
97
+ setMessage('')
98
+
99
+ const formData = new FormData(e.currentTarget)
100
+ const email = formData.get('email') as string
101
+ const password = formData.get('password') as string
102
+ const name = formData.get('name') as string
103
+
104
+ try {
105
+ const result = await authClient.signUp.email({
106
+ email,
107
+ password,
108
+ name,
109
+ })
110
+
111
+ if (result.error) {
112
+ setError(
113
+ result.error.message || 'Registration failed. Please try again.'
114
+ )
115
+ return
116
+ }
117
+
118
+ setMessage('Check your email to verify your account')
119
+ e.currentTarget.reset()
120
+ } catch (err) {
121
+ setError('Registration failed. Please try again.')
122
+ } finally {
123
+ setLoading(false)
124
+ }
125
+ }
126
+
127
+ return (
128
+ <Card className="w-full max-w-md mx-auto">
129
+ <CardHeader>
130
+ <CardTitle>Create an account</CardTitle>
131
+ <CardDescription>
132
+ Enter your information to create your account
133
+ </CardDescription>
134
+ </CardHeader>
135
+ <CardContent>
136
+ <form onSubmit={handleSubmit} className="space-y-4">
137
+ <div className="space-y-2">
138
+ <label
139
+ htmlFor="name"
140
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
141
+ >
142
+ Full Name
143
+ </label>
144
+ <Input
145
+ id="name"
146
+ name="name"
147
+ placeholder="John Doe"
148
+ required
149
+ disabled={loading}
150
+ />
151
+ </div>
152
+ <div className="space-y-2">
153
+ <label
154
+ htmlFor="email"
155
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
156
+ >
157
+ Email
158
+ </label>
159
+ <Input
160
+ id="email"
161
+ name="email"
162
+ type="email"
163
+ placeholder="john@example.com"
164
+ required
165
+ disabled={loading}
166
+ />
167
+ </div>
168
+ <div className="space-y-2">
169
+ <label
170
+ htmlFor="password"
171
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
172
+ >
173
+ Password
174
+ </label>
175
+ <Input
176
+ id="password"
177
+ name="password"
178
+ type="password"
179
+ placeholder="••••••••"
180
+ required
181
+ minLength={8}
182
+ disabled={loading}
183
+ />
184
+ </div>
185
+ {error && (
186
+ <div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
187
+ {error}
188
+ </div>
189
+ )}
190
+ {message && (
191
+ <div className="p-3 text-sm text-green-600 bg-green-50 border border-green-200 rounded-md">
192
+ {message}
193
+ </div>
194
+ )}
195
+ <Button type="submit" disabled={loading} className="w-full">
196
+ {loading ? 'Creating account...' : 'Sign up'}
197
+ </Button>
198
+ </form>
199
+ </CardContent>
200
+ </Card>
201
+ )
202
+ }
@@ -0,0 +1,27 @@
1
+ import { useLocation } from '@tanstack/react-router'
2
+ import { Header } from './header'
3
+ import { Sidebar } from './sidebar'
4
+
5
+ interface DashboardLayoutProps {
6
+ children: React.ReactNode
7
+ userName?: string | null
8
+ onSignOut?: () => Promise<void>
9
+ }
10
+
11
+ export function DashboardLayout({
12
+ children,
13
+ userName,
14
+ onSignOut,
15
+ }: DashboardLayoutProps) {
16
+ const location = useLocation()
17
+
18
+ return (
19
+ <div className="min-h-screen bg-gray-50">
20
+ <Header userName={userName} onSignOut={onSignOut} />
21
+ <div className="flex">
22
+ <Sidebar currentPath={location.pathname} />
23
+ <main className="flex-1 p-8">{children}</main>
24
+ </div>
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,29 @@
1
+ import { Link } from '@tanstack/react-router'
2
+ import { Button } from '@/components/ui/button'
3
+
4
+ interface HeaderProps {
5
+ userName?: string | null
6
+ onSignOut?: () => Promise<void>
7
+ }
8
+
9
+ export function Header({ userName, onSignOut }: HeaderProps) {
10
+ return (
11
+ <header className="bg-white border-b border-gray-200 h-16">
12
+ <div className="container mx-auto px-4 h-full">
13
+ <div className="flex justify-between items-center h-full">
14
+ <Link to="/" className="text-xl font-bold text-blue-600">
15
+ Recurring Rabbit
16
+ </Link>
17
+ <div className="flex items-center gap-4">
18
+ {userName && <span className="text-gray-700">{userName}</span>}
19
+ {onSignOut && (
20
+ <Button variant="outline" size="sm" onClick={onSignOut}>
21
+ Sign Out
22
+ </Button>
23
+ )}
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </header>
28
+ )
29
+ }
@@ -0,0 +1,38 @@
1
+ import { Link } from '@tanstack/react-router'
2
+
3
+ interface SidebarProps {
4
+ currentPath?: string
5
+ }
6
+
7
+ export function Sidebar({ currentPath }: SidebarProps) {
8
+ const navItems = [
9
+ { to: '/dashboard', label: 'Dashboard' },
10
+ { to: '/dashboard/settings', label: 'Settings' },
11
+ ]
12
+
13
+ return (
14
+ <aside className="w-64 bg-white border-r border-gray-200 min-h-[calc(100vh-64px)]">
15
+ <nav className="p-4">
16
+ <ul className="space-y-2">
17
+ {navItems.map((item) => {
18
+ const isActive = currentPath?.startsWith(item.to)
19
+ return (
20
+ <li key={item.to}>
21
+ <Link
22
+ to={item.to}
23
+ className={`block px-4 py-2 rounded-md text-sm font-medium transition-colors ${
24
+ isActive
25
+ ? 'bg-blue-50 text-blue-700'
26
+ : 'text-gray-700 hover:bg-gray-100'
27
+ }`}
28
+ >
29
+ {item.label}
30
+ </Link>
31
+ </li>
32
+ )
33
+ })}
34
+ </ul>
35
+ </nav>
36
+ </aside>
37
+ )
38
+ }
@@ -0,0 +1,57 @@
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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
+ destructive:
14
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15
+ outline:
16
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17
+ secondary:
18
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ },
22
+ size: {
23
+ default: 'h-10 px-4 py-2',
24
+ sm: 'h-9 rounded-md px-3',
25
+ lg: 'h-11 rounded-md px-8',
26
+ icon: 'h-10 w-10',
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: 'default',
31
+ size: 'default',
32
+ },
33
+ }
34
+ )
35
+
36
+ export interface ButtonProps
37
+ extends
38
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {
40
+ asChild?: boolean
41
+ }
42
+
43
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
45
+ const Comp = asChild ? Slot : 'button'
46
+ return (
47
+ <Comp
48
+ className={cn(buttonVariants({ variant, size, className }))}
49
+ ref={ref}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+ )
55
+ Button.displayName = 'Button'
56
+
57
+ export { Button, buttonVariants }
@@ -0,0 +1,79 @@
1
+ import * as React from 'react'
2
+
3
+ import { cn } from '@/lib/utils'
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ 'rounded-lg border bg-card text-card-foreground shadow-sm',
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = 'Card'
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn('flex flex-col space-y-1.5 p-6', className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = 'CardHeader'
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLParagraphElement,
34
+ React.HTMLAttributes<HTMLHeadingElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <h3
37
+ ref={ref}
38
+ className={cn(
39
+ 'text-2xl font-semibold leading-none tracking-tight',
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ ))
45
+ CardTitle.displayName = 'CardTitle'
46
+
47
+ const CardDescription = React.forwardRef<
48
+ HTMLParagraphElement,
49
+ React.HTMLAttributes<HTMLParagraphElement>
50
+ >(({ className, ...props }, ref) => (
51
+ <p
52
+ ref={ref}
53
+ className={cn('text-sm text-muted-foreground', className)}
54
+ {...props}
55
+ />
56
+ ))
57
+ CardDescription.displayName = 'CardDescription'
58
+
59
+ const CardContent = React.forwardRef<
60
+ HTMLDivElement,
61
+ React.HTMLAttributes<HTMLDivElement>
62
+ >(({ className, ...props }, ref) => (
63
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
64
+ ))
65
+ CardContent.displayName = 'CardContent'
66
+
67
+ const CardFooter = React.forwardRef<
68
+ HTMLDivElement,
69
+ React.HTMLAttributes<HTMLDivElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <div
72
+ ref={ref}
73
+ className={cn('flex items-center p-6 pt-0', className)}
74
+ {...props}
75
+ />
76
+ ))
77
+ CardFooter.displayName = 'CardFooter'
78
+
79
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }