@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,171 @@
1
+ import { createFileRoute, useRouter, useSearch } from '@tanstack/react-router'
2
+ import { useEffect, useState } from 'react'
3
+ import { authClient } from '@/lib/auth'
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
+ export const Route = createFileRoute('/auth/reset-password')({
15
+ component: ResetPassword,
16
+ })
17
+
18
+ function ResetPassword() {
19
+ const router = useRouter()
20
+ const search = useSearch({ from: '/auth/reset-password' })
21
+ const [password, setPassword] = useState('')
22
+ const [confirmPassword, setConfirmPassword] = useState('')
23
+ const [loading, setLoading] = useState(false)
24
+ const [message, setMessage] = useState('')
25
+ const [error, setError] = useState('')
26
+ const [tokenValid, setTokenValid] = useState(false)
27
+
28
+ useEffect(() => {
29
+ if (search.token) {
30
+ setTokenValid(true)
31
+ } else {
32
+ setError('Invalid reset link. Please request a new password reset.')
33
+ }
34
+ }, [search.token])
35
+
36
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
37
+ e.preventDefault()
38
+ setLoading(true)
39
+ setMessage('')
40
+ setError('')
41
+
42
+ if (password !== confirmPassword) {
43
+ setError('Passwords do not match.')
44
+ setLoading(false)
45
+ return
46
+ }
47
+
48
+ if (password.length < 8) {
49
+ setError('Password must be at least 8 characters.')
50
+ setLoading(false)
51
+ return
52
+ }
53
+
54
+ try {
55
+ await authClient.resetPassword({
56
+ newPassword: password,
57
+ token: search.token as string,
58
+ })
59
+
60
+ setMessage(
61
+ 'Password reset successful! You can now sign in with your new password.'
62
+ )
63
+ setTimeout(() => {
64
+ router.navigate({ to: '/auth/login' })
65
+ }, 2000)
66
+ } catch (err) {
67
+ setError('Failed to reset password. The link may have expired.')
68
+ console.error(err)
69
+ } finally {
70
+ setLoading(false)
71
+ }
72
+ }
73
+
74
+ return (
75
+ <div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
76
+ <Card className="w-full max-w-md">
77
+ <CardHeader>
78
+ <CardTitle>Reset Password</CardTitle>
79
+ <CardDescription>Enter your new password below</CardDescription>
80
+ </CardHeader>
81
+ <CardContent>
82
+ {!tokenValid ? (
83
+ <div className="space-y-4">
84
+ <div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
85
+ {error}
86
+ </div>
87
+ <Button
88
+ onClick={() => router.navigate({ to: '/auth/forgot-password' })}
89
+ className="w-full"
90
+ >
91
+ Request New Reset Link
92
+ </Button>
93
+ </div>
94
+ ) : message ? (
95
+ <div className="space-y-4">
96
+ <div className="p-3 text-sm text-green-700 bg-green-50 border border-green-200 rounded-md">
97
+ {message}
98
+ </div>
99
+ <p className="text-sm text-gray-500 text-center">
100
+ Redirecting to sign in...
101
+ </p>
102
+ </div>
103
+ ) : (
104
+ <form onSubmit={handleSubmit} className="space-y-4">
105
+ <div className="space-y-2">
106
+ <label
107
+ htmlFor="password"
108
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
109
+ >
110
+ New Password
111
+ </label>
112
+ <Input
113
+ id="password"
114
+ type="password"
115
+ value={password}
116
+ onChange={(e) => setPassword(e.target.value)}
117
+ placeholder="Enter new password"
118
+ required
119
+ disabled={loading}
120
+ minLength={8}
121
+ />
122
+ <p className="text-xs text-gray-500">
123
+ Must be at least 8 characters
124
+ </p>
125
+ </div>
126
+
127
+ <div className="space-y-2">
128
+ <label
129
+ htmlFor="confirmPassword"
130
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
131
+ >
132
+ Confirm Password
133
+ </label>
134
+ <Input
135
+ id="confirmPassword"
136
+ type="password"
137
+ value={confirmPassword}
138
+ onChange={(e) => setConfirmPassword(e.target.value)}
139
+ placeholder="Confirm new password"
140
+ required
141
+ disabled={loading}
142
+ minLength={8}
143
+ />
144
+ </div>
145
+
146
+ {error && (
147
+ <div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
148
+ {error}
149
+ </div>
150
+ )}
151
+
152
+ <Button type="submit" disabled={loading} className="w-full">
153
+ {loading ? 'Resetting...' : 'Reset Password'}
154
+ </Button>
155
+
156
+ <div className="text-center">
157
+ <button
158
+ type="button"
159
+ onClick={() => router.navigate({ to: '/auth/login' })}
160
+ className="text-sm text-gray-600 hover:text-gray-900"
161
+ >
162
+ Back to Sign In
163
+ </button>
164
+ </div>
165
+ </form>
166
+ )}
167
+ </CardContent>
168
+ </Card>
169
+ </div>
170
+ )
171
+ }
@@ -0,0 +1,111 @@
1
+ import { createFileRoute, useRouter, useSearch } from '@tanstack/react-router'
2
+ import { useEffect } from 'react'
3
+ import { Button } from '@/components/ui/button'
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from '@/components/ui/card'
11
+
12
+ export const Route = createFileRoute('/auth/verify-email')({
13
+ component: VerifyEmail,
14
+ })
15
+
16
+ function VerifyEmail() {
17
+ const router = useRouter()
18
+ const search = useSearch({ from: '/auth/verify-email' })
19
+ const error = search.error as string | null
20
+ const success = search.success as string | null
21
+
22
+ useEffect(() => {
23
+ if (success === 'true') {
24
+ const timeout = setTimeout(() => {
25
+ router.navigate({ to: '/auth/login' })
26
+ }, 3000)
27
+ return () => clearTimeout(timeout)
28
+ }
29
+ }, [success, router])
30
+
31
+ return (
32
+ <div className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
33
+ <Card className="w-full max-w-md">
34
+ <CardHeader>
35
+ <CardTitle>Email Verification</CardTitle>
36
+ <CardDescription>
37
+ {error ? 'Verification Failed' : 'Verification Complete'}
38
+ </CardDescription>
39
+ </CardHeader>
40
+ <CardContent className="space-y-4">
41
+ {success === 'true' ? (
42
+ <div className="text-center py-4">
43
+ <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100 mb-4">
44
+ <svg
45
+ className="w-8 h-8 text-green-600"
46
+ fill="none"
47
+ stroke="currentColor"
48
+ viewBox="0 0 24 24"
49
+ >
50
+ <path
51
+ strokeLinecap="round"
52
+ strokeLinejoin="round"
53
+ strokeWidth={2}
54
+ d="M5 13l4 4L19 7"
55
+ />
56
+ </svg>
57
+ </div>
58
+ <p className="text-gray-700">
59
+ Your email has been verified! You can now sign in.
60
+ </p>
61
+ <p className="text-sm text-gray-500 mt-2">
62
+ Redirecting to sign in...
63
+ </p>
64
+ </div>
65
+ ) : error ? (
66
+ <div className="text-center py-4">
67
+ <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-red-100 mb-4">
68
+ <svg
69
+ className="w-8 h-8 text-red-600"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ viewBox="0 0 24 24"
73
+ >
74
+ <path
75
+ strokeLinecap="round"
76
+ strokeLinejoin="round"
77
+ strokeWidth={2}
78
+ d="M6 18L18 6M6 6l12 12"
79
+ />
80
+ </svg>
81
+ </div>
82
+ <p className="text-gray-700 mb-4">
83
+ {error === 'invalid_token'
84
+ ? 'Invalid verification link. Please request a new verification email.'
85
+ : error === 'expired_token'
86
+ ? 'Verification link has expired. Please request a new verification email.'
87
+ : 'Verification failed. Please try again.'}
88
+ </p>
89
+ <div className="space-x-2">
90
+ <Button onClick={() => router.navigate({ to: '/auth/login' })}>
91
+ Sign In
92
+ </Button>
93
+ <Button
94
+ variant="outline"
95
+ onClick={() => router.navigate({ to: '/auth/register' })}
96
+ >
97
+ Register
98
+ </Button>
99
+ </div>
100
+ </div>
101
+ ) : (
102
+ <div className="text-center py-4">
103
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 mx-auto mb-4" />
104
+ <p className="text-gray-700">Verifying your email...</p>
105
+ </div>
106
+ )}
107
+ </CardContent>
108
+ </Card>
109
+ </div>
110
+ )
111
+ }
@@ -0,0 +1,122 @@
1
+ import { createFileRoute, useRouter, redirect } from '@tanstack/react-router'
2
+ import { useQuery } from '@tanstack/react-query'
3
+ import { useTRPC } from '@/lib/api'
4
+ import { signOut } from '@/lib/auth'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
6
+ import { DashboardLayout } from '@/components/layout/dashboard-layout'
7
+
8
+ export const Route = createFileRoute('/dashboard/')({
9
+ beforeLoad: async ({ location }) => {
10
+ if (typeof window === 'undefined') {
11
+ return
12
+ }
13
+ try {
14
+ const response = await fetch('/api/auth/get-session', {
15
+ headers: {
16
+ cookie: document.cookie,
17
+ },
18
+ })
19
+ const session = await response.json()
20
+ if (!session?.user) {
21
+ throw redirect({
22
+ to: '/auth/login',
23
+ search: { redirect: location.href },
24
+ })
25
+ }
26
+ } catch (error) {
27
+ if (error instanceof Error && error.message.includes('redirect')) {
28
+ throw error
29
+ }
30
+ throw redirect({
31
+ to: '/auth/login',
32
+ search: { redirect: location.href },
33
+ })
34
+ }
35
+ },
36
+ component: Dashboard,
37
+ })
38
+
39
+ function Dashboard() {
40
+ const router = useRouter()
41
+ const trpc = useTRPC()
42
+ const { data: stats } = useQuery(trpc.dashboard.getStats.queryOptions())
43
+
44
+ const handleSignOut = async () => {
45
+ await signOut()
46
+ router.navigate({ to: '/' as const })
47
+ }
48
+
49
+ return (
50
+ <DashboardLayout
51
+ userName={stats?.currentUser?.name}
52
+ onSignOut={handleSignOut}
53
+ >
54
+ <div className="max-w-6xl">
55
+ <h1 className="text-3xl font-bold mb-8">Dashboard</h1>
56
+
57
+ <div className="grid md:grid-cols-2 gap-6">
58
+ <Card>
59
+ <CardHeader>
60
+ <CardTitle>User Stats</CardTitle>
61
+ </CardHeader>
62
+ <CardContent>
63
+ <div className="space-y-4">
64
+ <div>
65
+ <p className="text-sm text-gray-600">Total Users</p>
66
+ <p className="text-3xl font-bold">{stats?.totalUsers ?? 0}</p>
67
+ </div>
68
+ <div>
69
+ <p className="text-sm text-gray-600">Active Users</p>
70
+ <p className="text-3xl font-bold">
71
+ {stats?.activeUsers ?? 0}
72
+ </p>
73
+ </div>
74
+ </div>
75
+ </CardContent>
76
+ </Card>
77
+
78
+ <Card>
79
+ <CardHeader>
80
+ <CardTitle>Your Profile</CardTitle>
81
+ </CardHeader>
82
+ <CardContent>
83
+ <div className="space-y-3">
84
+ <div>
85
+ <p className="text-sm text-gray-600">Name</p>
86
+ <p className="font-medium">{stats?.currentUser?.name}</p>
87
+ </div>
88
+ <div>
89
+ <p className="text-sm text-gray-600">Email</p>
90
+ <p className="font-medium">{stats?.currentUser?.email}</p>
91
+ </div>
92
+ <div>
93
+ <p className="text-sm text-gray-600">Email Verified</p>
94
+ <p className="font-medium">
95
+ {stats?.currentUser?.emailVerified ? 'Yes' : 'No'}
96
+ </p>
97
+ </div>
98
+ </div>
99
+ </CardContent>
100
+ </Card>
101
+ </div>
102
+
103
+ <div className="mt-8 bg-blue-50 p-6 rounded-lg border border-blue-200">
104
+ <h2 className="text-xl font-semibold mb-2">
105
+ Welcome to your dashboard!
106
+ </h2>
107
+ <p className="text-gray-700">
108
+ You've successfully set up your micro-SaaS starter. From here, you
109
+ can:
110
+ </p>
111
+ <ul className="mt-4 space-y-2 text-gray-700 list-disc list-inside">
112
+ <li>Add new features and pages</li>
113
+ <li>Explore the database schema in src/server/db/schema.ts</li>
114
+ <li>Create new tRPC procedures in src/server/api/routers/</li>
115
+ <li>Customize the UI components in src/components/</li>
116
+ <li>Deploy to Vercel, Railway, or your preferred platform</li>
117
+ </ul>
118
+ </div>
119
+ </div>
120
+ </DashboardLayout>
121
+ )
122
+ }
@@ -0,0 +1,161 @@
1
+ import { createFileRoute, useRouter, redirect } from '@tanstack/react-router'
2
+ import React, { useState } from 'react'
3
+ import { useQuery, useMutation } from '@tanstack/react-query'
4
+ import { useTRPC } from '@/lib/api'
5
+ import { signOut } from '@/lib/auth'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Input } from '@/components/ui/input'
8
+ import {
9
+ Card,
10
+ CardContent,
11
+ CardDescription,
12
+ CardHeader,
13
+ CardTitle,
14
+ } from '@/components/ui/card'
15
+ import { DashboardLayout } from '@/components/layout/dashboard-layout'
16
+
17
+ export const Route = createFileRoute('/dashboard/settings')({
18
+ beforeLoad: async ({ location }) => {
19
+ if (typeof window === 'undefined') {
20
+ return
21
+ }
22
+ try {
23
+ const response = await fetch('/api/auth/get-session', {
24
+ headers: {
25
+ cookie: document.cookie,
26
+ },
27
+ })
28
+ const session = await response.json()
29
+ if (!session?.user) {
30
+ throw redirect({
31
+ to: '/auth/login',
32
+ search: { redirect: location.href },
33
+ })
34
+ }
35
+ } catch (error) {
36
+ if (error instanceof Error && error.message.includes('redirect')) {
37
+ throw error
38
+ }
39
+ throw redirect({
40
+ to: '/auth/login',
41
+ search: { redirect: location.href },
42
+ })
43
+ }
44
+ },
45
+ component: Settings,
46
+ })
47
+
48
+ function Settings() {
49
+ const router = useRouter()
50
+ const trpc = useTRPC()
51
+ const { data: user } = useQuery(trpc.user.getProfile.queryOptions())
52
+ const updateProfile = useMutation(trpc.user.updateProfile.mutationOptions())
53
+
54
+ const [name, setName] = useState('')
55
+ const [loading, setLoading] = useState(false)
56
+ const [message, setMessage] = useState('')
57
+ const [error, setError] = useState('')
58
+
59
+ React.useEffect(() => {
60
+ if (user) {
61
+ setName(user.name)
62
+ }
63
+ }, [user])
64
+
65
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
66
+ e.preventDefault()
67
+ setLoading(true)
68
+ setMessage('')
69
+ setError('')
70
+
71
+ try {
72
+ const result = await updateProfile.mutateAsync({
73
+ name,
74
+ })
75
+
76
+ setMessage('Profile updated successfully!')
77
+ setName(result.name)
78
+ } catch (err) {
79
+ setError('Failed to update profile. Please try again.')
80
+ console.error(err)
81
+ } finally {
82
+ setLoading(false)
83
+ }
84
+ }
85
+
86
+ const handleSignOut = async () => {
87
+ await signOut()
88
+ router.navigate({ to: '/' as const })
89
+ }
90
+
91
+ return (
92
+ <DashboardLayout userName={user?.name} onSignOut={handleSignOut}>
93
+ <div className="max-w-2xl">
94
+ <h1 className="text-3xl font-bold mb-8">Settings</h1>
95
+
96
+ <Card>
97
+ <CardHeader>
98
+ <CardTitle>Profile Settings</CardTitle>
99
+ <CardDescription>Update your account information</CardDescription>
100
+ </CardHeader>
101
+ <CardContent>
102
+ <form onSubmit={handleSubmit} className="space-y-4">
103
+ <div className="space-y-2">
104
+ <label
105
+ htmlFor="name"
106
+ className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
107
+ >
108
+ Name
109
+ </label>
110
+ <Input
111
+ id="name"
112
+ type="text"
113
+ value={name}
114
+ onChange={(e) => setName(e.target.value)}
115
+ placeholder="Your name"
116
+ required
117
+ disabled={loading}
118
+ />
119
+ </div>
120
+
121
+ <div className="space-y-2">
122
+ <label
123
+ htmlFor="email-display"
124
+ className="text-sm font-medium leading-none text-gray-600"
125
+ >
126
+ Email
127
+ </label>
128
+ <Input
129
+ id="email-display"
130
+ type="email"
131
+ value={user?.email ?? ''}
132
+ disabled={true}
133
+ className="bg-gray-50"
134
+ />
135
+ <p className="text-xs text-gray-500">
136
+ Email cannot be changed. Contact support for updates.
137
+ </p>
138
+ </div>
139
+
140
+ {message && (
141
+ <div className="p-3 text-sm text-green-700 bg-green-50 border border-green-200 rounded-md">
142
+ {message}
143
+ </div>
144
+ )}
145
+
146
+ {error && (
147
+ <div className="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded-md">
148
+ {error}
149
+ </div>
150
+ )}
151
+
152
+ <Button type="submit" disabled={loading}>
153
+ {loading ? 'Saving...' : 'Save Changes'}
154
+ </Button>
155
+ </form>
156
+ </CardContent>
157
+ </Card>
158
+ </div>
159
+ </DashboardLayout>
160
+ )
161
+ }
@@ -0,0 +1,55 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --primary: 221.2 83.2% 53.3%;
12
+ --primary-foreground: 210 40% 98%;
13
+ --secondary: 210 40% 96.1%;
14
+ --secondary-foreground: 222.2 47.4% 11.2%;
15
+ --muted: 210 40% 96.1%;
16
+ --muted-foreground: 215.4 16.3% 46.9%;
17
+ --accent: 210 40% 96.1%;
18
+ --accent-foreground: 222.2 47.4% 11.2%;
19
+ --destructive: 0 84.2% 60.2%;
20
+ --destructive-foreground: 210 40% 98%;
21
+ --border: 214.3 31.8% 91.4%;
22
+ --input: 214.3 31.8% 91.4%;
23
+ --ring: 221.2 83.2% 53.3%;
24
+ --radius: 0.5rem;
25
+ }
26
+
27
+ .dark {
28
+ --background: 222.2 84% 4.9%;
29
+ --foreground: 210 40% 98%;
30
+ --card: 222.2 84% 4.9%;
31
+ --card-foreground: 210 40% 98%;
32
+ --primary: 217.2 91.2% 59.8%;
33
+ --primary-foreground: 222.2 84% 4.9%;
34
+ --secondary: 217.2 32.6% 17.5%;
35
+ --secondary-foreground: 210 40% 98%;
36
+ --muted: 217.2 32.6% 17.5%;
37
+ --muted-foreground: 215 20.2% 65.1%;
38
+ --accent: 217.2 32.6% 17.5%;
39
+ --accent-foreground: 210 40% 98%;
40
+ --destructive: 0 62.8% 30.6%;
41
+ --destructive-foreground: 210 40% 98%;
42
+ --border: 217.2 32.6% 17.5%;
43
+ --input: 217.2 32.6% 17.5%;
44
+ --ring: 224.3 76.3% 48%;
45
+ }
46
+ }
47
+
48
+ @layer base {
49
+ * {
50
+ @apply border-border;
51
+ }
52
+ body {
53
+ @apply bg-background text-foreground;
54
+ }
55
+ }