create-ej-app 17.0.0 → 19.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 (116) hide show
  1. package/README.md +89 -13
  2. package/cli.js +9 -25
  3. package/cli.ts +10 -34
  4. package/package.json +2 -3
  5. package/template/api/.env.example +3 -0
  6. package/template/api/.github/copilot-instructions.md +129 -0
  7. package/template/api/.vscode/extensions.json +9 -0
  8. package/template/api/.vscode/settings.json +3 -0
  9. package/template/api/Dockerfile +23 -0
  10. package/template/api/package.json +31 -0
  11. package/template/api/prettier.config.mjs +11 -0
  12. package/template/api/src/data-access/todo.ts +142 -0
  13. package/template/api/src/db/fields.ts +46 -0
  14. package/template/api/src/db/index.ts +21 -0
  15. package/template/api/src/db/schema.ts +19 -0
  16. package/template/api/src/env.ts +24 -0
  17. package/template/api/src/index.ts +39 -0
  18. package/template/api/src/lib/util.ts +55 -0
  19. package/template/api/src/routes/index.ts +18 -0
  20. package/template/api/src/routes/todo/index.ts +241 -0
  21. package/template/api/src/scripts/seed.ts +25 -0
  22. package/template/api/tsconfig.json +33 -0
  23. package/template/nextjs/.vscode/extensions.json +11 -0
  24. package/template/nextjs/.vscode/settings.json +3 -0
  25. package/template/{base → nextjs}/components.json +4 -1
  26. package/template/nextjs/drizzle.config.ts +10 -0
  27. package/template/{base → nextjs}/package.json +25 -27
  28. package/template/{base → nextjs}/src/app/(admin)/layout.tsx +2 -2
  29. package/template/nextjs/src/app/(app)/app/page.tsx +3 -0
  30. package/template/{base → nextjs}/src/app/(app)/layout.tsx +1 -1
  31. package/template/{base → nextjs}/src/app/(auth)/login/email-login-form.tsx +33 -34
  32. package/template/{base → nextjs}/src/app/globals.css +127 -15
  33. package/template/{base → nextjs}/src/app/layout.tsx +8 -4
  34. package/template/{base → nextjs}/src/app/not-found.tsx +1 -1
  35. package/template/nextjs/src/components/theme-provider.tsx +67 -0
  36. package/template/nextjs/src/components/ui/button.tsx +67 -0
  37. package/template/{base → nextjs}/src/components/ui/card.tsx +17 -6
  38. package/template/nextjs/src/components/ui/field.tsx +238 -0
  39. package/template/nextjs/src/components/ui/input.tsx +19 -0
  40. package/template/{base → nextjs}/src/components/ui/label.tsx +1 -1
  41. package/template/{base → nextjs}/src/components/ui/separator.tsx +2 -2
  42. package/template/{base → nextjs}/src/components/ui/sonner.tsx +5 -0
  43. package/template/nextjs/src/db/common.ts +31 -0
  44. package/template/{base → nextjs}/src/db/schema.ts +2 -28
  45. package/template/nextjs/src/hooks/use-mobile.ts +19 -0
  46. package/template/{base → nextjs}/src/lib/auth.ts +2 -6
  47. package/template/nextjs/src/lib/format-currency.ts +27 -0
  48. package/template/nextjs/src/lib/format-date.ts +12 -0
  49. package/template/nextjs/src/scripts/seed.ts +11 -0
  50. package/template/{base → nextjs}/src/scripts/test-mail.ts +11 -5
  51. package/template/{base → nextjs}/tsconfig.json +2 -0
  52. package/template/base/src/app/api/[[...slugs]]/route.ts +0 -25
  53. package/template/base/src/components/ui/button.tsx +0 -60
  54. package/template/base/src/components/ui/form.tsx +0 -167
  55. package/template/base/src/components/ui/input.tsx +0 -21
  56. package/template/base/src/lib/eden.ts +0 -7
  57. /package/template/{base → api}/drizzle.config.ts +0 -0
  58. /package/template/{base → nextjs}/.dockerignore +0 -0
  59. /package/template/{base → nextjs}/.env.example +0 -0
  60. /package/template/{base → nextjs}/Dockerfile +0 -0
  61. /package/template/{base → nextjs}/eslint.config.mjs +0 -0
  62. /package/template/{base → nextjs}/next.config.ts +0 -0
  63. /package/template/{base → nextjs}/postcss.config.mjs +0 -0
  64. /package/template/{base → nextjs}/prettier.config.mjs +0 -0
  65. /package/template/{base → nextjs}/public/placeholder.svg +0 -0
  66. /package/template/{base → nextjs}/src/app/(admin)/admin/page.tsx +0 -0
  67. /package/template/{base → nextjs}/src/app/(auth)/login/page.tsx +0 -0
  68. /package/template/{base → nextjs}/src/app/(auth)/login/social-login-buttons.tsx +0 -0
  69. /package/template/{base → nextjs}/src/app/(marketing)/layout.tsx +0 -0
  70. /package/template/{base → nextjs}/src/app/(marketing)/page.tsx +0 -0
  71. /package/template/{base → nextjs}/src/app/(marketing)/privacy/page.tsx +0 -0
  72. /package/template/{base → nextjs}/src/app/(marketing)/terms/page.tsx +0 -0
  73. /package/template/{base → nextjs}/src/app/api/auth/[...all]/route.ts +0 -0
  74. /package/template/{base → nextjs}/src/app/favicon.ico +0 -0
  75. /package/template/{base → nextjs}/src/components/loader.tsx +0 -0
  76. /package/template/{base → nextjs}/src/components/react-query-provider.tsx +0 -0
  77. /package/template/{base → nextjs}/src/components/screen-size.tsx +0 -0
  78. /package/template/{base → nextjs}/src/components/site-footer.tsx +0 -0
  79. /package/template/{base → nextjs}/src/components/theme-toggle.tsx +0 -0
  80. /package/template/{base → nextjs}/src/db/index.ts +0 -0
  81. /package/template/{base → nextjs}/src/emails/components/email-layout.tsx +0 -0
  82. /package/template/{base → nextjs}/src/emails/magic-link.tsx +0 -0
  83. /package/template/{base → nextjs}/src/emails/test.tsx +0 -0
  84. /package/template/{base → nextjs}/src/emails/welcome.tsx +0 -0
  85. /package/template/{base → nextjs}/src/env.ts +0 -0
  86. /package/template/{base → nextjs}/src/instrumentation.ts +0 -0
  87. /package/template/{base → nextjs}/src/lib/auth-client.ts +0 -0
  88. /package/template/{base → nextjs}/src/lib/cn.ts +0 -0
  89. /package/template/{base → nextjs}/src/lib/delay.ts +0 -0
  90. /package/template/{base → nextjs}/src/lib/email-service.ts +0 -0
  91. /package/template/{base → nextjs}/src/lib/nodemailer.ts +0 -0
  92. /package/template/{base → nextjs}/src/lib/otel.ts +0 -0
  93. /package/template/{base → nextjs}/src/lib/safe-action-client.ts +0 -0
  94. /package/template/{base → nextjs}/src/lib/safe-action.ts +0 -0
  95. /package/template/{base → nextjs}/src/lib/siteConfig.ts +0 -0
  96. /package/template/{base → nextjs}/src/lib/toast-message.ts +0 -0
  97. /package/template/{base → nextjs}/terraform/artifact-registry.tf +0 -0
  98. /package/template/{base → nextjs}/terraform/backend-service.tf +0 -0
  99. /package/template/{base → nextjs}/terraform/cleanup.sh +0 -0
  100. /package/template/{base → nextjs}/terraform/cloudbuild.tf +0 -0
  101. /package/template/{base → nextjs}/terraform/cloudrun.tf +0 -0
  102. /package/template/{base → nextjs}/terraform/database.tf +0 -0
  103. /package/template/{base → nextjs}/terraform/forwarding-rule.tf +0 -0
  104. /package/template/{base → nextjs}/terraform/ip-address.tf +0 -0
  105. /package/template/{base → nextjs}/terraform/network-endpoint-group.tf +0 -0
  106. /package/template/{base → nextjs}/terraform/network.tf +0 -0
  107. /package/template/{base → nextjs}/terraform/pre.sh +0 -0
  108. /package/template/{base → nextjs}/terraform/providers.tf +0 -0
  109. /package/template/{base → nextjs}/terraform/secret.txt +0 -0
  110. /package/template/{base → nextjs}/terraform/setup.sh +0 -0
  111. /package/template/{base → nextjs}/terraform/ssl-certs.tf +0 -0
  112. /package/template/{base → nextjs}/terraform/target-https-proxy copy.tf +0 -0
  113. /package/template/{base → nextjs}/terraform/target-https-proxy.tf +0 -0
  114. /package/template/{base → nextjs}/terraform/terraform.tfvars +0 -0
  115. /package/template/{base → nextjs}/terraform/url-map.tf +0 -0
  116. /package/template/{base → nextjs}/terraform/variables.tf +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://ui.shadcn.com/schema.json",
3
- "style": "new-york",
3
+ "style": "radix-vega",
4
4
  "rsc": true,
5
5
  "tsx": true,
6
6
  "tailwind": {
@@ -11,6 +11,7 @@
11
11
  "prefix": ""
12
12
  },
13
13
  "iconLibrary": "lucide",
14
+ "rtl": false,
14
15
  "aliases": {
15
16
  "components": "~/components",
16
17
  "utils": "~/lib/cn",
@@ -18,5 +19,7 @@
18
19
  "lib": "~/lib",
19
20
  "hooks": "~/hooks"
20
21
  },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
21
24
  "registries": {}
22
25
  }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit'
2
+ import { env } from '~/env'
3
+
4
+ export default defineConfig({
5
+ schema: './src/db/schema.ts',
6
+ dialect: 'postgresql',
7
+ dbCredentials: {
8
+ url: env.DATABASE_URL
9
+ }
10
+ })
@@ -13,66 +13,64 @@
13
13
  "analyze": "ANALYZE=true bun --bun next build --webpack",
14
14
  "start": "bun --bun next start",
15
15
  "lint": "eslint",
16
- "type-check": "tsc --noEmit --incremental false",
16
+ "type-check": "tsgo --incremental false",
17
17
  "format": "prettier --write \"./src/**/*.{js,cjs,mjs,jsx,ts,tsx}\"",
18
18
  "db:push": "drizzle-kit push",
19
+ "db:seed": "bun ./src/scripts/seed.ts",
19
20
  "db:studio": "drizzle-kit studio",
20
21
  "gen:auth": "bunx @better-auth/cli generate"
21
22
  },
22
23
  "dependencies": {
23
24
  "@ejekanshjain/simple-email-validator": "1.0.3",
24
- "@elysiajs/eden": "1.4.8",
25
25
  "@hookform/resolvers": "5.2.2",
26
- "@next/third-parties": "16.1.6",
26
+ "@next/third-parties": "16.2.1",
27
27
  "@opentelemetry/api": "1.9.0",
28
- "@opentelemetry/sdk-trace-base": "2.5.1",
28
+ "@opentelemetry/sdk-trace-base": "2.6.0",
29
29
  "@paralleldrive/cuid2": "3.3.0",
30
- "@radix-ui/react-label": "2.1.8",
31
- "@radix-ui/react-separator": "1.1.8",
32
- "@radix-ui/react-slot": "1.2.4",
33
- "@react-email/components": "1.0.7",
30
+ "@react-email/components": "1.0.10",
34
31
  "@react-email/render": "2.0.4",
35
32
  "@t3-oss/env-nextjs": "0.13.10",
36
- "@tanstack/react-query": "5.90.21",
33
+ "@tanstack/react-query": "5.91.3",
37
34
  "@vercel/otel": "2.1.1",
38
- "better-auth": "1.4.18",
35
+ "better-auth": "1.5.5",
39
36
  "class-variance-authority": "0.7.1",
40
37
  "clsx": "2.1.1",
41
38
  "drizzle-orm": "0.45.1",
42
- "elysia": "1.4.25",
43
- "lucide-react": "0.564.0",
44
- "next": "16.1.6",
45
- "next-safe-action": "8.0.11",
39
+ "lucide-react": "0.577.0",
40
+ "next": "16.2.1",
41
+ "next-safe-action": "8.1.8",
46
42
  "next-themes": "0.4.6",
47
43
  "nextjs-toploader": "3.9.17",
48
- "nodemailer": "8.0.1",
44
+ "nodemailer": "8.0.3",
49
45
  "postgres": "3.4.8",
46
+ "radix-ui": "1.4.3",
50
47
  "react": "19.2.4",
51
48
  "react-dom": "19.2.4",
52
- "react-hook-form": "7.71.1",
49
+ "react-hook-form": "7.71.2",
53
50
  "sonner": "2.0.7",
54
- "tailwind-merge": "3.4.1",
51
+ "tailwind-merge": "3.5.0",
55
52
  "tw-animate-css": "1.4.0",
56
- "workflow": "4.1.0-beta.57",
53
+ "workflow": "4.2.0-beta.71",
57
54
  "zod": "4.3.6"
58
55
  },
59
56
  "devDependencies": {
60
- "@next/bundle-analyzer": "16.1.6",
61
- "@react-email/preview-server": "5.2.8",
62
- "@tailwindcss/postcss": "4.1.18",
63
- "@types/bun": "1.3.9",
64
- "@types/nodemailer": "7.0.10",
57
+ "@next/bundle-analyzer": "16.2.1",
58
+ "@react-email/preview-server": "5.2.10",
59
+ "@tailwindcss/postcss": "4.2.2",
60
+ "@types/bun": "1.3.11",
61
+ "@types/nodemailer": "7.0.11",
65
62
  "@types/react": "19.2.14",
66
63
  "@types/react-dom": "19.2.3",
64
+ "@typescript/native-preview": "7.0.0-dev.20260320.1",
67
65
  "babel-plugin-react-compiler": "1.0.0",
68
- "drizzle-kit": "0.31.9",
66
+ "drizzle-kit": "0.31.10",
69
67
  "eslint": "9.39.2",
70
- "eslint-config-next": "16.1.6",
68
+ "eslint-config-next": "16.2.1",
71
69
  "prettier": "3.8.1",
72
70
  "prettier-plugin-organize-imports": "4.3.0",
73
71
  "prettier-plugin-tailwindcss": "0.7.2",
74
- "react-email": "5.2.8",
75
- "tailwindcss": "4.1.18",
72
+ "react-email": "5.2.10",
73
+ "tailwindcss": "4.2.2",
76
74
  "typescript": "5.9.3"
77
75
  },
78
76
  "trustedDependencies": [
@@ -9,11 +9,11 @@ export default async function Layout({
9
9
  const authSession = await getAuthSession()
10
10
 
11
11
  if (!authSession?.isAdmin) {
12
- notFound()
12
+ return notFound()
13
13
  }
14
14
 
15
15
  return (
16
- <div className="flex min-h-screen flex-col">
16
+ <div className="flex min-h-screen flex-col space-y-4">
17
17
  <main className="flex-1">{children}</main>
18
18
  </div>
19
19
  )
@@ -0,0 +1,3 @@
1
+ export default async function Page() {
2
+ return <div>Hello, World!</div>
3
+ }
@@ -9,7 +9,7 @@ export default async function Layout({
9
9
  const authSession = await getAuthSession()
10
10
 
11
11
  if (!authSession) {
12
- redirect('/login')
12
+ return redirect('/login')
13
13
  }
14
14
 
15
15
  return (
@@ -3,17 +3,15 @@
3
3
  import { zodResolver } from '@hookform/resolvers/zod'
4
4
  import { Mail } from 'lucide-react'
5
5
  import { useState } from 'react'
6
- import { useForm } from 'react-hook-form'
6
+ import { Controller, useForm } from 'react-hook-form'
7
7
  import { z } from 'zod'
8
8
  import { Button } from '~/components/ui/button'
9
9
  import {
10
- Form,
11
- FormControl,
12
- FormField,
13
- FormItem,
14
- FormLabel,
15
- FormMessage
16
- } from '~/components/ui/form'
10
+ Field,
11
+ FieldContent,
12
+ FieldError,
13
+ FieldLabel
14
+ } from '~/components/ui/field'
17
15
  import { Input } from '~/components/ui/input'
18
16
  import { signIn } from '~/lib/auth-client'
19
17
  import { toastErrorMessage, toastSuccessMessage } from '~/lib/toast-message'
@@ -75,31 +73,32 @@ export const EmailLoginForm = () => {
75
73
  }
76
74
 
77
75
  return (
78
- <Form {...form}>
79
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
80
- <FormField
81
- control={form.control}
82
- name="email"
83
- render={({ field }) => (
84
- <FormItem>
85
- <FormLabel>Email</FormLabel>
86
- <FormControl>
87
- <Input
88
- placeholder="name@example.com"
89
- autoCapitalize="off"
90
- autoComplete="email"
91
- autoFocus
92
- {...field}
93
- />
94
- </FormControl>
95
- <FormMessage />
96
- </FormItem>
97
- )}
98
- />
99
- <Button type="submit" className="w-full" disabled={isLoading}>
100
- {isLoading ? 'Sending...' : 'Send Magic Link'}
101
- </Button>
102
- </form>
103
- </Form>
76
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
77
+ <Controller
78
+ control={form.control}
79
+ name="email"
80
+ render={({ field, fieldState }) => (
81
+ <Field data-invalid={fieldState.invalid}>
82
+ <FieldLabel htmlFor="email">Email</FieldLabel>
83
+ <FieldContent>
84
+ <Input
85
+ id="email"
86
+ type="email"
87
+ placeholder="name@example.com"
88
+ autoCapitalize="off"
89
+ autoComplete="email"
90
+ autoFocus
91
+ aria-invalid={fieldState.invalid}
92
+ {...field}
93
+ />
94
+ <FieldError errors={[fieldState.error]} />
95
+ </FieldContent>
96
+ </Field>
97
+ )}
98
+ />
99
+ <Button type="submit" className="w-full" disabled={isLoading}>
100
+ {isLoading ? 'Sending...' : 'Send Magic Link'}
101
+ </Button>
102
+ </form>
104
103
  )
105
104
  }
@@ -4,10 +4,6 @@
4
4
  @custom-variant dark (&:is(.dark *));
5
5
 
6
6
  @theme inline {
7
- --radius-sm: calc(var(--radius) - 4px);
8
- --radius-md: calc(var(--radius) - 2px);
9
- --radius-lg: var(--radius);
10
- --radius-xl: calc(var(--radius) + 4px);
11
7
  --color-background: var(--background);
12
8
  --color-foreground: var(--foreground);
13
9
  --color-card: var(--card);
@@ -23,6 +19,7 @@
23
19
  --color-accent: var(--accent);
24
20
  --color-accent-foreground: var(--accent-foreground);
25
21
  --color-destructive: var(--destructive);
22
+ --color-destructive-foreground: var(--destructive-foreground);
26
23
  --color-border: var(--border);
27
24
  --color-input: var(--input);
28
25
  --color-ring: var(--ring);
@@ -39,6 +36,18 @@
39
36
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
40
37
  --color-sidebar-border: var(--sidebar-border);
41
38
  --color-sidebar-ring: var(--sidebar-ring);
39
+
40
+ --font-heading: var(--font-geist-sans);
41
+ --font-sans: var(--font-geist-sans);
42
+ --font-mono: var(--font-geist-mono);
43
+
44
+ --radius-sm: calc(var(--radius) * 0.6);
45
+ --radius-md: calc(var(--radius) * 0.8);
46
+ --radius-lg: var(--radius);
47
+ --radius-xl: calc(var(--radius) * 1.4);
48
+ --radius-2xl: calc(var(--radius) * 1.8);
49
+ --radius-3xl: calc(var(--radius) * 2.2);
50
+ --radius-4xl: calc(var(--radius) * 2.6);
42
51
  }
43
52
 
44
53
  :root {
@@ -58,14 +67,15 @@
58
67
  --accent: oklch(0.97 0 0);
59
68
  --accent-foreground: oklch(0.205 0 0);
60
69
  --destructive: oklch(0.577 0.245 27.325);
70
+ --destructive-foreground: oklch(1 0 0);
61
71
  --border: oklch(0.922 0 0);
62
72
  --input: oklch(0.922 0 0);
63
73
  --ring: oklch(0.708 0 0);
64
- --chart-1: oklch(0.646 0.222 41.116);
65
- --chart-2: oklch(0.6 0.118 184.704);
66
- --chart-3: oklch(0.398 0.07 227.392);
67
- --chart-4: oklch(0.828 0.189 84.429);
68
- --chart-5: oklch(0.769 0.188 70.08);
74
+ --chart-1: oklch(0.87 0 0);
75
+ --chart-2: oklch(0.556 0 0);
76
+ --chart-3: oklch(0.439 0 0);
77
+ --chart-4: oklch(0.371 0 0);
78
+ --chart-5: oklch(0.269 0 0);
69
79
  --sidebar: oklch(0.985 0 0);
70
80
  --sidebar-foreground: oklch(0.145 0 0);
71
81
  --sidebar-primary: oklch(0.205 0 0);
@@ -92,14 +102,15 @@
92
102
  --accent: oklch(0.269 0 0);
93
103
  --accent-foreground: oklch(0.985 0 0);
94
104
  --destructive: oklch(0.704 0.191 22.216);
105
+ --destructive-foreground: oklch(0.985 0 0);
95
106
  --border: oklch(1 0 0 / 10%);
96
107
  --input: oklch(1 0 0 / 15%);
97
108
  --ring: oklch(0.556 0 0);
98
- --chart-1: oklch(0.488 0.243 264.376);
99
- --chart-2: oklch(0.696 0.17 162.48);
100
- --chart-3: oklch(0.769 0.188 70.08);
101
- --chart-4: oklch(0.627 0.265 303.9);
102
- --chart-5: oklch(0.645 0.246 16.439);
109
+ --chart-1: oklch(0.87 0 0);
110
+ --chart-2: oklch(0.556 0 0);
111
+ --chart-3: oklch(0.439 0 0);
112
+ --chart-4: oklch(0.371 0 0);
113
+ --chart-5: oklch(0.269 0 0);
103
114
  --sidebar: oklch(0.205 0 0);
104
115
  --sidebar-foreground: oklch(0.985 0 0);
105
116
  --sidebar-primary: oklch(0.488 0.243 264.376);
@@ -114,15 +125,116 @@
114
125
  * {
115
126
  @apply border-border outline-ring/50;
116
127
  }
128
+
129
+ html {
130
+ @apply font-sans antialiased;
131
+ }
132
+
117
133
  body {
118
- @apply text-foreground bg-background min-h-screen font-sans antialiased;
134
+ @apply text-foreground bg-background min-h-screen;
119
135
  }
136
+
120
137
  button:not([disabled]),
121
138
  [role='button']:not([disabled]) {
122
139
  cursor: pointer;
123
140
  }
124
141
  }
125
142
 
143
+ @theme inline {
144
+ @keyframes accordion-down {
145
+ from {
146
+ height: 0;
147
+ }
148
+ to {
149
+ height: var(
150
+ --radix-accordion-content-height,
151
+ var(--accordion-panel-height, auto)
152
+ );
153
+ }
154
+ }
155
+
156
+ @keyframes accordion-up {
157
+ from {
158
+ height: var(
159
+ --radix-accordion-content-height,
160
+ var(--accordion-panel-height, auto)
161
+ );
162
+ }
163
+ to {
164
+ height: 0;
165
+ }
166
+ }
167
+ }
168
+
169
+ @custom-variant data-open {
170
+ &:where([data-state='open']),
171
+ &:where([data-open]:not([data-open='false'])) {
172
+ @slot;
173
+ }
174
+ }
175
+
176
+ @custom-variant data-closed {
177
+ &:where([data-state='closed']),
178
+ &:where([data-closed]:not([data-closed='false'])) {
179
+ @slot;
180
+ }
181
+ }
182
+
183
+ @custom-variant data-checked {
184
+ &:where([data-state='checked']),
185
+ &:where([data-checked]:not([data-checked='false'])) {
186
+ @slot;
187
+ }
188
+ }
189
+
190
+ @custom-variant data-unchecked {
191
+ &:where([data-state='unchecked']),
192
+ &:where([data-unchecked]:not([data-unchecked='false'])) {
193
+ @slot;
194
+ }
195
+ }
196
+
197
+ @custom-variant data-selected {
198
+ &:where([data-selected='true']) {
199
+ @slot;
200
+ }
201
+ }
202
+
203
+ @custom-variant data-disabled {
204
+ &:where([data-disabled='true']),
205
+ &:where([data-disabled]:not([data-disabled='false'])) {
206
+ @slot;
207
+ }
208
+ }
209
+
210
+ @custom-variant data-active {
211
+ &:where([data-state='active']),
212
+ &:where([data-active]:not([data-active='false'])) {
213
+ @slot;
214
+ }
215
+ }
216
+
217
+ @custom-variant data-horizontal {
218
+ &:where([data-orientation='horizontal']) {
219
+ @slot;
220
+ }
221
+ }
222
+
223
+ @custom-variant data-vertical {
224
+ &:where([data-orientation='vertical']) {
225
+ @slot;
226
+ }
227
+ }
228
+
229
+ @utility no-scrollbar {
230
+ -ms-overflow-style: none;
231
+ scrollbar-width: none;
232
+
233
+ &::-webkit-scrollbar {
234
+ display: none;
235
+ }
236
+ }
237
+
126
238
  @utility container {
127
239
  @apply mx-auto p-4;
128
240
  }
@@ -1,9 +1,9 @@
1
1
  import type { Metadata } from 'next'
2
- import { ThemeProvider } from 'next-themes'
3
2
  import { Geist, Geist_Mono } from 'next/font/google'
4
3
  import { Loader } from '~/components/loader'
5
4
  import { ReactQueryProvider } from '~/components/react-query-provider'
6
5
  import { ScreenSize } from '~/components/screen-size'
6
+ import { ThemeProvider } from '~/components/theme-provider'
7
7
  import { Toaster } from '~/components/ui/sonner'
8
8
  import { env } from '~/env'
9
9
  import { siteConfig } from '~/lib/siteConfig'
@@ -30,10 +30,14 @@ export default function RootLayout({
30
30
  children: React.ReactNode
31
31
  }>) {
32
32
  return (
33
- <html lang="en" suppressHydrationWarning>
34
- <body className={`${geistSans.variable} ${geistMono.variable}`}>
33
+ <html
34
+ lang="en"
35
+ suppressHydrationWarning
36
+ className={`${geistSans.variable} ${geistMono.variable}`}
37
+ >
38
+ <body>
35
39
  <ReactQueryProvider>
36
- <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
40
+ <ThemeProvider>
37
41
  <Loader />
38
42
  {children}
39
43
  {env.APP_ENV === 'development' ? <ScreenSize /> : null}
@@ -1,8 +1,8 @@
1
1
  'use client'
2
2
 
3
- import { Button } from '@/components/ui/button'
4
3
  import Link from 'next/link'
5
4
  import { usePathname } from 'next/navigation'
5
+ import { Button } from '~/components/ui/button'
6
6
 
7
7
  export default function NotFound() {
8
8
  const pathname = usePathname()
@@ -0,0 +1,67 @@
1
+ 'use client'
2
+
3
+ import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes'
4
+ import { FC, useEffect } from 'react'
5
+
6
+ function isTypingTarget(target: EventTarget | null) {
7
+ if (!(target instanceof HTMLElement)) {
8
+ return false
9
+ }
10
+
11
+ return (
12
+ target.isContentEditable ||
13
+ target.tagName === 'INPUT' ||
14
+ target.tagName === 'TEXTAREA' ||
15
+ target.tagName === 'SELECT'
16
+ )
17
+ }
18
+
19
+ function ThemeHotkey() {
20
+ const { resolvedTheme, setTheme } = useTheme()
21
+
22
+ useEffect(() => {
23
+ function onKeyDown(event: KeyboardEvent) {
24
+ if (event.defaultPrevented || event.repeat) {
25
+ return
26
+ }
27
+
28
+ if (event.metaKey || event.ctrlKey || event.altKey) {
29
+ return
30
+ }
31
+
32
+ if (event.key.toLowerCase() !== 'd') {
33
+ return
34
+ }
35
+
36
+ if (isTypingTarget(event.target)) {
37
+ return
38
+ }
39
+
40
+ setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
41
+ }
42
+
43
+ window.addEventListener('keydown', onKeyDown)
44
+
45
+ return () => {
46
+ window.removeEventListener('keydown', onKeyDown)
47
+ }
48
+ }, [resolvedTheme, setTheme])
49
+
50
+ return null
51
+ }
52
+
53
+ export const ThemeProvider: FC<{
54
+ children: React.ReactNode
55
+ }> = ({ children }) => {
56
+ return (
57
+ <NextThemesProvider
58
+ attribute="class"
59
+ defaultTheme="system"
60
+ enableSystem
61
+ disableTransitionOnChange
62
+ >
63
+ <ThemeHotkey />
64
+ {children}
65
+ </NextThemesProvider>
66
+ )
67
+ }
@@ -0,0 +1,67 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import { Slot } from 'radix-ui'
3
+ import * as React from 'react'
4
+
5
+ import { cn } from '~/lib/cn'
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
13
+ outline:
14
+ 'border-border bg-background shadow-xs hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
15
+ secondary:
16
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
17
+ ghost:
18
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
19
+ destructive:
20
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
21
+ link: 'text-primary underline-offset-4 hover:underline'
22
+ },
23
+ size: {
24
+ default:
25
+ 'h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
26
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
27
+ sm: 'h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5',
28
+ lg: 'h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3',
29
+ icon: 'size-9',
30
+ 'icon-xs':
31
+ "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
32
+ 'icon-sm':
33
+ 'size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md',
34
+ 'icon-lg': 'size-10'
35
+ }
36
+ },
37
+ defaultVariants: {
38
+ variant: 'default',
39
+ size: 'default'
40
+ }
41
+ }
42
+ )
43
+
44
+ function Button({
45
+ className,
46
+ variant = 'default',
47
+ size = 'default',
48
+ asChild = false,
49
+ ...props
50
+ }: React.ComponentProps<'button'> &
51
+ VariantProps<typeof buttonVariants> & {
52
+ asChild?: boolean
53
+ }) {
54
+ const Comp = asChild ? Slot.Root : 'button'
55
+
56
+ return (
57
+ <Comp
58
+ data-slot="button"
59
+ data-variant={variant}
60
+ data-size={size}
61
+ className={cn(buttonVariants({ variant, size, className }))}
62
+ {...props}
63
+ />
64
+ )
65
+ }
66
+
67
+ export { Button, buttonVariants }
@@ -2,12 +2,17 @@ import * as React from 'react'
2
2
 
3
3
  import { cn } from '~/lib/cn'
4
4
 
5
- function Card({ className, ...props }: React.ComponentProps<'div'>) {
5
+ function Card({
6
+ className,
7
+ size = 'default',
8
+ ...props
9
+ }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
6
10
  return (
7
11
  <div
8
12
  data-slot="card"
13
+ data-size={size}
9
14
  className={cn(
10
- 'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
15
+ 'group/card bg-card text-card-foreground ring-foreground/10 flex flex-col gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
11
16
  className
12
17
  )}
13
18
  {...props}
@@ -20,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
20
25
  <div
21
26
  data-slot="card-header"
22
27
  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',
28
+ 'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4',
24
29
  className
25
30
  )}
26
31
  {...props}
@@ -32,7 +37,10 @@ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
32
37
  return (
33
38
  <div
34
39
  data-slot="card-title"
35
- className={cn('leading-none font-semibold', className)}
40
+ className={cn(
41
+ 'font-heading text-base leading-normal font-medium group-data-[size=sm]/card:text-sm',
42
+ className
43
+ )}
36
44
  {...props}
37
45
  />
38
46
  )
@@ -65,7 +73,7 @@ function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
65
73
  return (
66
74
  <div
67
75
  data-slot="card-content"
68
- className={cn('px-6', className)}
76
+ className={cn('px-6 group-data-[size=sm]/card:px-4', className)}
69
77
  {...props}
70
78
  />
71
79
  )
@@ -75,7 +83,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
75
83
  return (
76
84
  <div
77
85
  data-slot="card-footer"
78
- className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
86
+ className={cn(
87
+ 'flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4',
88
+ className
89
+ )}
79
90
  {...props}
80
91
  />
81
92
  )