create-ej-app 26.0.0 → 28.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 (357) hide show
  1. package/README.md +125 -226
  2. package/cli.ts +79 -29
  3. package/package.json +2 -2
  4. package/template/api/.vscode/settings.json +1 -1
  5. package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
  6. package/template/api/CLAUDE.md +1 -0
  7. package/template/api/package.json +10 -10
  8. package/template/nextjs/.dockerignore +1 -0
  9. package/template/nextjs/AGENTS.md +260 -0
  10. package/template/nextjs/CLAUDE.md +1 -0
  11. package/template/nextjs/README.md +109 -0
  12. package/template/nextjs/bunfig.toml +3 -0
  13. package/template/nextjs/eslint.config.mjs +6 -5
  14. package/template/nextjs/gitignore +6 -1
  15. package/template/nextjs/next.config.ts +8 -1
  16. package/template/nextjs/package.json +47 -39
  17. package/template/nextjs/public/logo-dark.svg +4 -0
  18. package/template/nextjs/public/logo.svg +4 -0
  19. package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
  20. package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
  21. package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  22. package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
  23. package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
  24. package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
  25. package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
  26. package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  27. package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  28. package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
  29. package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  30. package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  31. package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
  32. package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
  33. package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
  34. package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
  35. package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  36. package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
  37. package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
  38. package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
  39. package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
  40. package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
  41. package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
  42. package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
  43. package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
  44. package/template/nextjs/src/app/error.tsx +37 -0
  45. package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  46. package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  47. package/template/nextjs/src/app/fonts/index.ts +60 -0
  48. package/template/nextjs/src/app/global-error.tsx +38 -0
  49. package/template/nextjs/src/app/globals.css +29 -0
  50. package/template/nextjs/src/app/layout.tsx +45 -18
  51. package/template/nextjs/src/app/not-found.tsx +25 -3
  52. package/template/nextjs/src/app/robots.ts +29 -0
  53. package/template/nextjs/src/app/sitemap.ts +21 -0
  54. package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
  55. package/template/nextjs/src/components/dashboard-header.tsx +70 -0
  56. package/template/nextjs/src/components/data-table/enum.ts +5 -0
  57. package/template/nextjs/src/components/data-table/features.ts +49 -0
  58. package/template/nextjs/src/components/data-table/index.tsx +961 -0
  59. package/template/nextjs/src/components/data-table/types.ts +76 -0
  60. package/template/nextjs/src/components/error.tsx +71 -0
  61. package/template/nextjs/src/components/loader.tsx +18 -2
  62. package/template/nextjs/src/components/logo.tsx +49 -0
  63. package/template/nextjs/src/components/navigation-search.tsx +140 -0
  64. package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
  65. package/template/nextjs/src/components/page-heading.tsx +38 -0
  66. package/template/nextjs/src/components/react-query-provider.tsx +41 -2
  67. package/template/nextjs/src/components/route-error.tsx +55 -0
  68. package/template/nextjs/src/components/screen-size.tsx +36 -0
  69. package/template/nextjs/src/components/site-footer.tsx +20 -4
  70. package/template/nextjs/src/components/theme-provider.tsx +48 -0
  71. package/template/nextjs/src/components/theme-toggle.tsx +58 -5
  72. package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
  73. package/template/nextjs/src/components/ui/avatar.tsx +112 -0
  74. package/template/nextjs/src/components/ui/badge.tsx +54 -0
  75. package/template/nextjs/src/components/ui/button.tsx +3 -1
  76. package/template/nextjs/src/components/ui/card.tsx +4 -4
  77. package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
  78. package/template/nextjs/src/components/ui/command.tsx +192 -0
  79. package/template/nextjs/src/components/ui/dialog.tsx +164 -0
  80. package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
  81. package/template/nextjs/src/components/ui/input-group.tsx +156 -0
  82. package/template/nextjs/src/components/ui/select.tsx +195 -0
  83. package/template/nextjs/src/components/ui/sheet.tsx +143 -0
  84. package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
  85. package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
  86. package/template/nextjs/src/components/ui/table.tsx +116 -0
  87. package/template/nextjs/src/components/ui/textarea.tsx +18 -0
  88. package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
  89. package/template/nextjs/src/components/user-menu.tsx +206 -0
  90. package/template/nextjs/src/db/common.ts +10 -0
  91. package/template/nextjs/src/db/index.ts +16 -6
  92. package/template/nextjs/src/db/relations.ts +21 -0
  93. package/template/nextjs/src/db/schema.ts +85 -46
  94. package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
  95. package/template/nextjs/src/emails/magic-link.tsx +15 -13
  96. package/template/nextjs/src/emails/test.tsx +11 -9
  97. package/template/nextjs/src/emails/welcome.tsx +12 -9
  98. package/template/nextjs/src/env.ts +1 -1
  99. package/template/nextjs/src/hooks/use-mobile.ts +14 -12
  100. package/template/nextjs/src/instrumentation.ts +20 -2
  101. package/template/nextjs/src/lib/actions.ts +55 -0
  102. package/template/nextjs/src/lib/admin-navigation.ts +43 -0
  103. package/template/nextjs/src/lib/auth-client.ts +20 -9
  104. package/template/nextjs/src/lib/auth-permissions.ts +61 -0
  105. package/template/nextjs/src/lib/auth.ts +33 -3
  106. package/template/nextjs/src/lib/callback-url.ts +28 -0
  107. package/template/nextjs/src/lib/constants.ts +21 -0
  108. package/template/nextjs/src/lib/delay.ts +10 -0
  109. package/template/nextjs/src/lib/email-service.ts +21 -0
  110. package/template/nextjs/src/lib/errors.ts +74 -0
  111. package/template/nextjs/src/lib/format-currency.ts +22 -24
  112. package/template/nextjs/src/lib/format-date.ts +69 -6
  113. package/template/nextjs/src/lib/nodemailer.ts +27 -3
  114. package/template/nextjs/src/lib/otel.ts +92 -2
  115. package/template/nextjs/src/lib/safe-action-client.ts +60 -9
  116. package/template/nextjs/src/lib/safe-action.ts +152 -16
  117. package/template/nextjs/src/lib/search.ts +16 -0
  118. package/template/nextjs/src/lib/seo.ts +7 -0
  119. package/template/nextjs/src/lib/siteConfig.ts +17 -1
  120. package/template/nextjs/src/lib/toast-message.ts +87 -1
  121. package/template/nextjs/src/lib/validations.ts +30 -0
  122. package/template/nextjs/src/proxy.ts +48 -0
  123. package/template/nextjs/src/scripts/reset-db.ts +22 -0
  124. package/template/nextjs/src/scripts/seed.ts +59 -8
  125. package/template/nextjs/src/scripts/test-mail.ts +28 -20
  126. package/template/nextjs/tests/admin/users.test.ts +64 -0
  127. package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
  128. package/template/nextjs/tests/db/seed.test.ts +49 -0
  129. package/template/nextjs/tests/helpers/action.ts +71 -0
  130. package/template/nextjs/tests/helpers/fixtures.ts +78 -0
  131. package/template/nextjs/tests/helpers/index.ts +2 -0
  132. package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
  133. package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
  134. package/template/nextjs/tests/lib/errors.test.ts +10 -0
  135. package/template/nextjs/tests/lib/search.test.ts +23 -0
  136. package/template/nextjs/tests/setup/pglite-db.ts +63 -0
  137. package/template/nextjs/tests/setup/preload.ts +95 -0
  138. package/template/nextjs/tsconfig.json +1 -0
  139. package/template/saas-starter/.dockerignore +1 -0
  140. package/template/saas-starter/.env.example +15 -15
  141. package/template/saas-starter/AGENTS.md +302 -0
  142. package/template/saas-starter/CLAUDE.md +1 -0
  143. package/template/saas-starter/README.md +124 -2
  144. package/template/saas-starter/bunfig.toml +3 -0
  145. package/template/saas-starter/eslint.config.mjs +6 -5
  146. package/template/saas-starter/gitignore +6 -1
  147. package/template/saas-starter/next.config.ts +19 -1
  148. package/template/saas-starter/package.json +50 -44
  149. package/template/saas-starter/public/logo-dark.svg +4 -0
  150. package/template/saas-starter/public/logo.svg +4 -0
  151. package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
  152. package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
  153. package/template/saas-starter/src/app/(admin)/actions/users.ts +145 -59
  154. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +4 -1
  155. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +12 -10
  156. package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
  157. package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
  158. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +24 -28
  159. package/template/saas-starter/src/app/(admin)/admin/page.tsx +59 -2
  160. package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
  161. package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  162. package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  163. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +372 -10
  164. package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  165. package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  166. package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
  167. package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
  168. package/template/saas-starter/src/app/(app)/actions/billing.ts +171 -107
  169. package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
  170. package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
  171. package/template/saas-starter/src/app/(app)/actions/members.ts +149 -123
  172. package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
  173. package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
  174. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +6 -9
  175. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +6 -9
  176. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +45 -53
  177. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +40 -47
  178. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +80 -105
  179. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +72 -82
  180. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +51 -52
  181. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
  182. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +15 -16
  183. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +77 -44
  184. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +11 -18
  185. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +14 -28
  186. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
  187. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
  188. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +15 -13
  189. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +15 -6
  190. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +6 -3
  191. package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
  192. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +5 -9
  193. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +68 -8
  194. package/template/saas-starter/src/app/(app)/app/layout.tsx +44 -32
  195. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +75 -87
  196. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +10 -12
  197. package/template/saas-starter/src/app/(app)/app/page.tsx +16 -21
  198. package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
  199. package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
  200. package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  201. package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
  202. package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
  203. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +49 -27
  204. package/template/saas-starter/src/app/(auth)/login/page.tsx +39 -21
  205. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +80 -18
  206. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +16 -10
  207. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +28 -42
  208. package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
  209. package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
  210. package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
  211. package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
  212. package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
  213. package/template/saas-starter/src/app/(marketing)/layout.tsx +38 -4
  214. package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
  215. package/template/saas-starter/src/app/(marketing)/page.tsx +23 -2
  216. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +62 -2
  217. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +40 -2
  218. package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
  219. package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
  220. package/template/saas-starter/src/app/actions/uploads.ts +36 -0
  221. package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
  222. package/template/saas-starter/src/app/error.tsx +37 -0
  223. package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  224. package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  225. package/template/saas-starter/src/app/fonts/index.ts +60 -0
  226. package/template/saas-starter/src/app/global-error.tsx +38 -0
  227. package/template/saas-starter/src/app/globals.css +29 -0
  228. package/template/saas-starter/src/app/layout.tsx +43 -19
  229. package/template/saas-starter/src/app/not-found.tsx +25 -3
  230. package/template/saas-starter/src/app/robots.ts +14 -10
  231. package/template/saas-starter/src/app/sitemap.ts +2 -1
  232. package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
  233. package/template/saas-starter/src/components/dashboard-header.tsx +7 -1
  234. package/template/saas-starter/src/components/data-table/features.ts +49 -0
  235. package/template/saas-starter/src/components/data-table/index.tsx +65 -64
  236. package/template/saas-starter/src/components/data-table/types.ts +5 -4
  237. package/template/saas-starter/src/components/error.tsx +45 -0
  238. package/template/saas-starter/src/components/file-upload.tsx +65 -26
  239. package/template/saas-starter/src/components/loader.tsx +18 -2
  240. package/template/saas-starter/src/components/logo.tsx +49 -0
  241. package/template/saas-starter/src/components/navigation-search.tsx +2 -2
  242. package/template/saas-starter/src/components/page-heading.tsx +6 -0
  243. package/template/saas-starter/src/components/react-query-provider.tsx +41 -2
  244. package/template/saas-starter/src/components/route-error.tsx +55 -0
  245. package/template/saas-starter/src/components/screen-size.tsx +36 -0
  246. package/template/saas-starter/src/components/site-footer.tsx +20 -4
  247. package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
  248. package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
  249. package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
  250. package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
  251. package/template/saas-starter/src/components/theme-provider.tsx +48 -0
  252. package/template/saas-starter/src/components/theme-toggle.tsx +58 -5
  253. package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
  254. package/template/saas-starter/src/components/ui/badge.tsx +6 -1
  255. package/template/saas-starter/src/components/ui/button.tsx +2 -0
  256. package/template/saas-starter/src/components/ui/command.tsx +1 -1
  257. package/template/saas-starter/src/components/user-menu.tsx +147 -24
  258. package/template/saas-starter/src/db/common.ts +33 -1
  259. package/template/saas-starter/src/db/enums.ts +24 -0
  260. package/template/saas-starter/src/db/index.ts +14 -5
  261. package/template/saas-starter/src/db/relations.ts +40 -3
  262. package/template/saas-starter/src/db/schema.ts +186 -35
  263. package/template/saas-starter/src/emails/components/email-layout.tsx +27 -11
  264. package/template/saas-starter/src/emails/invitation.tsx +34 -21
  265. package/template/saas-starter/src/emails/magic-link.tsx +15 -13
  266. package/template/saas-starter/src/emails/test.tsx +11 -9
  267. package/template/saas-starter/src/emails/welcome.tsx +12 -9
  268. package/template/saas-starter/src/env.ts +1 -1
  269. package/template/saas-starter/src/hooks/use-mobile.ts +14 -12
  270. package/template/saas-starter/src/instrumentation.ts +20 -2
  271. package/template/saas-starter/src/lib/actions.ts +34 -0
  272. package/template/saas-starter/src/lib/admin-navigation.ts +33 -1
  273. package/template/saas-starter/src/lib/app-navigation.ts +45 -41
  274. package/template/saas-starter/src/lib/auth-client.ts +8 -3
  275. package/template/saas-starter/src/lib/auth-permissions.ts +50 -11
  276. package/template/saas-starter/src/lib/auth.ts +122 -21
  277. package/template/saas-starter/src/lib/billing.ts +59 -24
  278. package/template/saas-starter/src/lib/callback-url.ts +28 -0
  279. package/template/saas-starter/src/lib/constants.ts +78 -9
  280. package/template/saas-starter/src/lib/delay.ts +10 -0
  281. package/template/saas-starter/src/lib/email-service.ts +30 -1
  282. package/template/saas-starter/src/lib/errors.ts +74 -0
  283. package/template/saas-starter/src/lib/format-currency.ts +22 -24
  284. package/template/saas-starter/src/lib/format-date.ts +69 -6
  285. package/template/saas-starter/src/lib/image-url.ts +54 -0
  286. package/template/saas-starter/src/lib/nodemailer.ts +27 -3
  287. package/template/saas-starter/src/lib/organization-access.ts +56 -76
  288. package/template/saas-starter/src/lib/otel.ts +92 -2
  289. package/template/saas-starter/src/lib/plan-limits.ts +37 -0
  290. package/template/saas-starter/src/lib/plans.ts +40 -13
  291. package/template/saas-starter/src/lib/rbac.ts +44 -0
  292. package/template/saas-starter/src/lib/safe-action-client.ts +60 -9
  293. package/template/saas-starter/src/lib/safe-action.ts +137 -17
  294. package/template/saas-starter/src/lib/search.ts +16 -0
  295. package/template/saas-starter/src/lib/seo.ts +7 -0
  296. package/template/saas-starter/src/lib/siteConfig.ts +17 -1
  297. package/template/saas-starter/src/lib/storage.ts +176 -46
  298. package/template/saas-starter/src/lib/stripe.ts +4 -1
  299. package/template/saas-starter/src/lib/toast-message.ts +87 -1
  300. package/template/saas-starter/src/lib/validations.ts +85 -2
  301. package/template/saas-starter/src/proxy.ts +48 -0
  302. package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
  303. package/template/saas-starter/src/scripts/seed.ts +82 -8
  304. package/template/saas-starter/src/scripts/setup-db.ts +72 -0
  305. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +13 -7
  306. package/template/saas-starter/src/scripts/test-mail.ts +28 -20
  307. package/template/saas-starter/tests/admin/users.test.ts +64 -0
  308. package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
  309. package/template/saas-starter/tests/db/seed.test.ts +57 -0
  310. package/template/saas-starter/tests/helpers/action.ts +71 -0
  311. package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
  312. package/template/saas-starter/tests/helpers/index.ts +2 -0
  313. package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
  314. package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
  315. package/template/saas-starter/tests/lib/errors.test.ts +10 -0
  316. package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
  317. package/template/saas-starter/tests/lib/search.test.ts +23 -0
  318. package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
  319. package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
  320. package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
  321. package/template/saas-starter/tests/setup/preload.ts +108 -0
  322. package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
  323. package/template/saas-starter/tsconfig.json +1 -0
  324. package/cli.js +0 -147
  325. package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
  326. package/template/nextjs/src/app/(app)/layout.tsx +0 -20
  327. package/template/nextjs/terraform/artifact-registry.tf +0 -5
  328. package/template/nextjs/terraform/backend-service.tf +0 -13
  329. package/template/nextjs/terraform/cleanup.sh +0 -9
  330. package/template/nextjs/terraform/cloudbuild.tf +0 -70
  331. package/template/nextjs/terraform/cloudrun.tf +0 -68
  332. package/template/nextjs/terraform/database.tf +0 -60
  333. package/template/nextjs/terraform/forwarding-rule.tf +0 -43
  334. package/template/nextjs/terraform/gitignore +0 -4
  335. package/template/nextjs/terraform/ip-address.tf +0 -11
  336. package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
  337. package/template/nextjs/terraform/network.tf +0 -21
  338. package/template/nextjs/terraform/pre.sh +0 -3
  339. package/template/nextjs/terraform/providers.tf +0 -18
  340. package/template/nextjs/terraform/secret.txt +0 -11
  341. package/template/nextjs/terraform/setup.sh +0 -25
  342. package/template/nextjs/terraform/ssl-certs.tf +0 -32
  343. package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
  344. package/template/nextjs/terraform/target-https-proxy.tf +0 -14
  345. package/template/nextjs/terraform/terraform.tfvars +0 -4
  346. package/template/nextjs/terraform/url-map.tf +0 -23
  347. package/template/nextjs/terraform/variables.tf +0 -21
  348. package/template/saas-starter/next-env.d.ts +0 -6
  349. package/template/saas-starter/src/actions/uploads.ts +0 -28
  350. package/template/saas-starter/src/actions/uploads.validation.ts +0 -14
  351. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +0 -183
  352. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +0 -129
  353. package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +0 -415
  354. package/template/saas-starter/src/app/(app)/actions/organization.ts +0 -92
  355. package/template/saas-starter/src/app/proxy.ts +0 -28
  356. package/template/saas-starter/src/components/impersonation-banner.tsx +0 -44
  357. package/template/saas-starter/src/lib/organization-plan-limits.ts +0 -38
@@ -0,0 +1,281 @@
1
+ 'use client'
2
+
3
+ import { Eye, Plus, Ticket } from 'lucide-react'
4
+ import Link from 'next/link'
5
+ import {
6
+ parseAsInteger,
7
+ parseAsJson,
8
+ parseAsString,
9
+ useQueryStates
10
+ } from 'nuqs'
11
+ import {
12
+ getAdminSupportTicketsAction,
13
+ getMyAccountSupportTicketsAction,
14
+ getOrganizationSupportTicketsAction,
15
+ getSupportTicketOrganizationsAction
16
+ } from '~/app/actions/support-tickets'
17
+ import { DataTable, type DataTableColumnDef } from '~/components/data-table'
18
+ import { SortOrderEnum } from '~/components/data-table/enum'
19
+ import { DataTableFilter } from '~/components/data-table/types'
20
+ import { PageHeading } from '~/components/page-heading'
21
+ import { Button } from '~/components/ui/button'
22
+ import { formatDate } from '~/lib/format-date'
23
+ import { useSafeActionQuery } from '~/lib/safe-action-client'
24
+ import { CreateSupportTicketDialog } from './create-support-ticket-dialog'
25
+ import {
26
+ SupportPriorityBadge,
27
+ SupportStatusBadge,
28
+ supportPriorityOptions,
29
+ supportStatusOptions
30
+ } from './support-ticket-shared'
31
+
32
+ type SupportTicket = {
33
+ id: string
34
+ ticketNumber: string
35
+ subject: string
36
+ status: string
37
+ priority: string | null
38
+ createdAt: Date
39
+ resolvedAt: Date | null
40
+ userId: string | null
41
+ userName: string | null
42
+ userEmail: string | null
43
+ organizationId: string | null
44
+ organizationName: string | null
45
+ }
46
+
47
+ const baseFilters: DataTableFilter[] = [
48
+ {
49
+ id: 'status',
50
+ label: 'Status',
51
+ options: supportStatusOptions.map(option => ({
52
+ label: option.label,
53
+ value: option.value
54
+ }))
55
+ },
56
+ {
57
+ id: 'priority',
58
+ label: 'Priority',
59
+ options: supportPriorityOptions.map(option => ({
60
+ label: option.label,
61
+ value: option.value
62
+ }))
63
+ }
64
+ ]
65
+
66
+ // Only the platform view spans organizations, so only it filters by one.
67
+ const adminFilters: DataTableFilter[] = [
68
+ ...baseFilters,
69
+ {
70
+ id: 'organizationId',
71
+ label: 'Organization',
72
+ action: getSupportTicketOrganizationsAction,
73
+ queryKey: 'support-ticket-organizations'
74
+ }
75
+ ]
76
+
77
+ type SupportTicketsTableProps =
78
+ | { mode: 'admin' | 'account'; basePath: string; organizationId?: undefined }
79
+ | { mode: 'app'; basePath: string; organizationId: string }
80
+
81
+ const COPY = {
82
+ admin: {
83
+ title: 'Support Tickets',
84
+ description: 'View, respond to, and manage customer support conversations.',
85
+ searchPlaceholder: 'Search by ticket #, subject, user, or organization…'
86
+ },
87
+ app: {
88
+ title: 'Support Tickets',
89
+ description:
90
+ 'Every ticket for this organization. Owners and admins can reply to all of them.',
91
+ searchPlaceholder: 'Search by ticket # or subject…'
92
+ },
93
+ account: {
94
+ title: 'Support',
95
+ description:
96
+ "Tickets about your account. To raise one about an organization, use that organization's support page.",
97
+ searchPlaceholder: 'Search by ticket # or subject…'
98
+ }
99
+ } as const
100
+
101
+ export function SupportTicketsTable({
102
+ mode,
103
+ basePath,
104
+ organizationId
105
+ }: SupportTicketsTableProps) {
106
+ const [queryState, setQueryState] = useQueryStates({
107
+ page: parseAsInteger.withDefault(1),
108
+ limit: parseAsInteger.withDefault(10),
109
+ sortBy: parseAsString,
110
+ sortOrder: parseAsString,
111
+ search: parseAsString,
112
+ filters: parseAsJson(v => v as Record<string, (string | boolean)[]>)
113
+ })
114
+
115
+ const params = {
116
+ page: queryState.page,
117
+ limit: queryState.limit,
118
+ sortBy: (queryState.sortBy ?? undefined) as
119
+ | 'ticketNumber'
120
+ | 'subject'
121
+ | 'status'
122
+ | 'priority'
123
+ | 'createdAt'
124
+ | 'resolvedAt'
125
+ | undefined,
126
+ sortOrder: (queryState.sortOrder ?? undefined) as SortOrderEnum | undefined,
127
+ search: queryState.search ?? undefined,
128
+ filters: queryState.filters ?? undefined
129
+ }
130
+
131
+ const adminQuery = useSafeActionQuery(
132
+ 'admin-support-tickets',
133
+ getAdminSupportTicketsAction,
134
+ params,
135
+ { enabled: mode === 'admin' }
136
+ )
137
+ const organizationQuery = useSafeActionQuery(
138
+ `support-tickets-${organizationId}`,
139
+ getOrganizationSupportTicketsAction,
140
+ { ...params, organizationId: organizationId! },
141
+ { enabled: mode === 'app' }
142
+ )
143
+ const accountQuery = useSafeActionQuery(
144
+ 'account-support-tickets',
145
+ getMyAccountSupportTicketsAction,
146
+ params,
147
+ { enabled: mode === 'account' }
148
+ )
149
+ const { data, isLoading } =
150
+ mode === 'admin'
151
+ ? adminQuery
152
+ : mode === 'app'
153
+ ? organizationQuery
154
+ : accountQuery
155
+
156
+ const columns: DataTableColumnDef<SupportTicket>[] = [
157
+ {
158
+ accessorKey: 'ticketNumber',
159
+ header: 'Ticket #',
160
+ enableSorting: true,
161
+ cell: ({ row }) => (
162
+ <span className="font-mono text-sm">{row.original.ticketNumber}</span>
163
+ )
164
+ },
165
+ {
166
+ accessorKey: 'subject',
167
+ header: mode === 'account' ? 'Subject' : 'Subject & Raised by',
168
+ enableSorting: true,
169
+ cell: ({ row }) => (
170
+ <div className="flex flex-col">
171
+ <span className="line-clamp-1 text-sm font-medium">
172
+ {row.original.subject}
173
+ </span>
174
+ {mode === 'account' ? null : (
175
+ <span className="text-muted-foreground text-xs">
176
+ {row.original.userName ||
177
+ row.original.userEmail ||
178
+ 'Removed user'}
179
+ </span>
180
+ )}
181
+ </div>
182
+ )
183
+ },
184
+ ...(mode === 'admin'
185
+ ? [
186
+ {
187
+ accessorKey: 'organizationName',
188
+ header: 'Organization',
189
+ enableSorting: false,
190
+ cell: ({ row }: { row: { original: SupportTicket } }) =>
191
+ row.original.organizationId ? (
192
+ <span className="line-clamp-1 text-sm">
193
+ {row.original.organizationName || 'Unknown'}
194
+ </span>
195
+ ) : (
196
+ <span className="text-muted-foreground text-sm">Account</span>
197
+ )
198
+ } satisfies DataTableColumnDef<SupportTicket>
199
+ ]
200
+ : []),
201
+ {
202
+ accessorKey: 'status',
203
+ header: 'Status',
204
+ enableSorting: true,
205
+ cell: ({ row }) => <SupportStatusBadge status={row.original.status} />
206
+ },
207
+ {
208
+ accessorKey: 'priority',
209
+ header: 'Priority',
210
+ enableSorting: true,
211
+ cell: ({ row }) => (
212
+ <SupportPriorityBadge priority={row.original.priority} />
213
+ )
214
+ },
215
+ {
216
+ accessorKey: 'createdAt',
217
+ header: 'Created',
218
+ enableSorting: true,
219
+ cell: ({ row }) => formatDate(row.original.createdAt, { short: true })
220
+ },
221
+ {
222
+ id: 'actions',
223
+ cell: ({ row }) => (
224
+ <Button asChild variant="ghost" size="icon" className="h-8 w-8">
225
+ <Link href={`${basePath}/${row.original.id}`}>
226
+ <Eye className="h-4 w-4" />
227
+ <span className="sr-only">View ticket</span>
228
+ </Link>
229
+ </Button>
230
+ )
231
+ }
232
+ ]
233
+
234
+ return (
235
+ <div className="space-y-6">
236
+ <PageHeading
237
+ title={COPY[mode].title}
238
+ description={COPY[mode].description}
239
+ icon={Ticket}
240
+ >
241
+ {mode === 'admin' ? null : (
242
+ <CreateSupportTicketDialog
243
+ organizationId={organizationId}
244
+ redirectBasePath={basePath}
245
+ >
246
+ <Button size="sm">
247
+ <Plus className="mr-2 h-4 w-4" />
248
+ New Ticket
249
+ </Button>
250
+ </CreateSupportTicketDialog>
251
+ )}
252
+ </PageHeading>
253
+
254
+ <DataTable
255
+ columns={columns}
256
+ data={data?.[0]}
257
+ isLoading={isLoading}
258
+ totalCount={data?.[1]}
259
+ enableSearch
260
+ initialSearch={params.search}
261
+ searchPlaceholder={COPY[mode].searchPlaceholder}
262
+ manualPagination
263
+ manualSorting
264
+ pageIndex={params.page - 1}
265
+ pageSize={params.limit}
266
+ filters={mode === 'admin' ? adminFilters : baseFilters}
267
+ activeFilters={params.filters ?? {}}
268
+ onParamsChange={({ page, limit, sortBy, sortOrder, search, filters }) =>
269
+ setQueryState({
270
+ page,
271
+ limit,
272
+ sortBy: sortBy ?? null,
273
+ sortOrder: sortOrder ?? null,
274
+ search: search ?? null,
275
+ filters: filters ?? null
276
+ })
277
+ }
278
+ />
279
+ </div>
280
+ )
281
+ }
@@ -1,8 +1,30 @@
1
+ /**
2
+ * @fileoverview Theme provider for light/dark mode and theme hotkeys.
3
+ *
4
+ * Wraps the application with Next Themes provider and manages theme switching.
5
+ * Includes a hotkey component (D key) to toggle the theme.
6
+ *
7
+ * **Features:**
8
+ * - System-aware default theme preference
9
+ * - Light/dark/system theme options
10
+ * - Keyboard shortcut (D key) for theme toggle
11
+ * - No theme transitions on page load
12
+ *
13
+ * @module components/theme-provider
14
+ */
15
+
1
16
  'use client'
2
17
 
3
18
  import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes'
4
19
  import { FC, useEffect } from 'react'
5
20
 
21
+ /**
22
+ * Determines if the target element is a text input field.
23
+ * Used to avoid triggering theme hotkey when typing in inputs.
24
+ *
25
+ * @param target - DOM event target to check
26
+ * @returns true if target is a contenteditable, input, textarea, or select element
27
+ */
6
28
  function isTypingTarget(target: EventTarget | null) {
7
29
  if (!(target instanceof HTMLElement)) {
8
30
  return false
@@ -16,10 +38,27 @@ function isTypingTarget(target: EventTarget | null) {
16
38
  )
17
39
  }
18
40
 
41
+ /**
42
+ * Theme Hotkey Component
43
+ *
44
+ * Listens for the 'D' key and toggles between light and dark themes.
45
+ * Ignores hotkey when user is typing in an input field.
46
+ *
47
+ * **Behavior:**
48
+ * - Press 'D' anywhere to toggle light ↔ dark
49
+ * - No effect if typing in input, textarea, or contenteditable element
50
+ * - Requires modifier keys (Ctrl, Cmd, Alt) to NOT be pressed
51
+ *
52
+ * @returns {null} Does not render any elements
53
+ */
19
54
  function ThemeHotkey() {
20
55
  const { resolvedTheme, setTheme } = useTheme()
21
56
 
22
57
  useEffect(() => {
58
+ /**
59
+ * Handles keyboard down events for theme toggle.
60
+ * Checks conditions and prevents default if hotkey is triggered.
61
+ */
23
62
  function onKeyDown(event: KeyboardEvent) {
24
63
  if (event.defaultPrevented || event.repeat) {
25
64
  return
@@ -50,6 +89,15 @@ function ThemeHotkey() {
50
89
  return null
51
90
  }
52
91
 
92
+ /**
93
+ * Theme Provider Component
94
+ *
95
+ * Wraps application with Next Themes provider and mounts theme hotkey.
96
+ *
97
+ * @param {Object} props - Component props
98
+ * @param {React.ReactNode} props.children - Child components to wrap
99
+ * @returns {JSX.Element} Provider wrapping children with theme support
100
+ */
53
101
  export const ThemeProvider: FC<{
54
102
  children: React.ReactNode
55
103
  }> = ({ children }) => {
@@ -1,22 +1,75 @@
1
+ /**
2
+ * @fileoverview Theme Toggle Button Component
3
+ *
4
+ * Cycles through light, dark, and system themes.
5
+ * Uses next-themes for theme state management.
6
+ *
7
+ * **Theme Cycle:**
8
+ * light → dark → system → light…
9
+ *
10
+ * **Icons:**
11
+ * - Sun: Light mode
12
+ * - Moon: Dark mode
13
+ * - Monitor: System preference
14
+ *
15
+ * **Hydration:**
16
+ * - Waits for client mount before rendering icons
17
+ * - Prevents hydration mismatch
18
+ *
19
+ * @module components/theme-toggle
20
+ */
21
+
1
22
  'use client'
2
23
 
3
- import { Moon, Sun } from 'lucide-react'
24
+ import { Monitor, Moon, Sun } from 'lucide-react'
4
25
  import { useTheme } from 'next-themes'
5
- import { FC } from 'react'
26
+ import { FC, useSyncExternalStore } from 'react'
6
27
  import { Button } from './ui/button'
7
28
 
29
+ const emptySubscribe = () => () => {}
30
+
31
+ /**
32
+ * Theme Toggle Button
33
+ *
34
+ * Ghost button that cycles through available themes.
35
+ * Shows appropriate icon for current theme state.
36
+ *
37
+ * **Behavior:**
38
+ * - Click cycles: light → dark → system → light
39
+ * - Shows nothing until mounted (prevents hydration issues)
40
+ *
41
+ * @returns {JSX.Element} Icon button for theme switching
42
+ */
8
43
  export const ThemeToggle: FC = () => {
9
44
  const { theme, setTheme } = useTheme()
45
+ // Client-only: server snapshot is false so SSR and the first client paint match.
46
+ const mounted = useSyncExternalStore(
47
+ emptySubscribe,
48
+ () => true,
49
+ () => false
50
+ )
51
+
52
+ const cycleTheme = () => {
53
+ const themes = ['light', 'dark', 'system']
54
+ const currentIndex = themes.indexOf(theme || 'system')
55
+ const nextIndex = (currentIndex + 1) % themes.length
56
+ setTheme(themes[nextIndex]!)
57
+ }
10
58
 
11
59
  return (
12
60
  <Button
13
61
  variant="ghost"
14
62
  size="icon"
63
+ onClick={cycleTheme}
15
64
  aria-label="Toggle theme"
16
- onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
17
65
  >
18
- <Sun className="hidden h-5 w-5 dark:inline" />
19
- <Moon className="inline h-5 w-5 dark:hidden" />
66
+ {!mounted ? null : (
67
+ <>
68
+ {theme === 'light' && <Sun className="size-4" />}
69
+ {theme === 'dark' && <Moon className="size-4" />}
70
+ {theme === 'system' && <Monitor className="size-4" />}
71
+ </>
72
+ )}
20
73
  </Button>
21
74
  )
22
75
  }
@@ -0,0 +1,199 @@
1
+ 'use client'
2
+
3
+ import { AlertDialog as AlertDialogPrimitive } from 'radix-ui'
4
+ import * as React from 'react'
5
+
6
+ import { Button } from '~/components/ui/button'
7
+ import { cn } from '~/lib/cn'
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18
+ return (
19
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ )
21
+ }
22
+
23
+ function AlertDialogPortal({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26
+ return (
27
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28
+ )
29
+ }
30
+
31
+ function AlertDialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35
+ return (
36
+ <AlertDialogPrimitive.Overlay
37
+ data-slot="alert-dialog-overlay"
38
+ className={cn(
39
+ 'data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs',
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function AlertDialogContent({
48
+ className,
49
+ size = 'default',
50
+ ...props
51
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
52
+ size?: 'default' | 'sm'
53
+ }) {
54
+ return (
55
+ <AlertDialogPortal>
56
+ <AlertDialogOverlay />
57
+ <AlertDialogPrimitive.Content
58
+ data-slot="alert-dialog-content"
59
+ data-size={size}
60
+ className={cn(
61
+ 'group/alert-dialog-content bg-popover text-popover-foreground ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl p-6 ring-1 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg',
62
+ className
63
+ )}
64
+ {...props}
65
+ />
66
+ </AlertDialogPortal>
67
+ )
68
+ }
69
+
70
+ function AlertDialogHeader({
71
+ className,
72
+ ...props
73
+ }: React.ComponentProps<'div'>) {
74
+ return (
75
+ <div
76
+ data-slot="alert-dialog-header"
77
+ className={cn(
78
+ 'grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]',
79
+ className
80
+ )}
81
+ {...props}
82
+ />
83
+ )
84
+ }
85
+
86
+ function AlertDialogFooter({
87
+ className,
88
+ ...props
89
+ }: React.ComponentProps<'div'>) {
90
+ return (
91
+ <div
92
+ data-slot="alert-dialog-footer"
93
+ className={cn(
94
+ 'flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end',
95
+ className
96
+ )}
97
+ {...props}
98
+ />
99
+ )
100
+ }
101
+
102
+ function AlertDialogMedia({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<'div'>) {
106
+ return (
107
+ <div
108
+ data-slot="alert-dialog-media"
109
+ className={cn(
110
+ "bg-muted mb-2 inline-flex size-16 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
111
+ className
112
+ )}
113
+ {...props}
114
+ />
115
+ )
116
+ }
117
+
118
+ function AlertDialogTitle({
119
+ className,
120
+ ...props
121
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
122
+ return (
123
+ <AlertDialogPrimitive.Title
124
+ data-slot="alert-dialog-title"
125
+ className={cn(
126
+ 'font-heading text-lg font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2',
127
+ className
128
+ )}
129
+ {...props}
130
+ />
131
+ )
132
+ }
133
+
134
+ function AlertDialogDescription({
135
+ className,
136
+ ...props
137
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
138
+ return (
139
+ <AlertDialogPrimitive.Description
140
+ data-slot="alert-dialog-description"
141
+ className={cn(
142
+ 'text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3',
143
+ className
144
+ )}
145
+ {...props}
146
+ />
147
+ )
148
+ }
149
+
150
+ function AlertDialogAction({
151
+ className,
152
+ variant = 'default',
153
+ size = 'default',
154
+ ...props
155
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
156
+ Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
157
+ return (
158
+ <Button variant={variant} size={size} asChild>
159
+ <AlertDialogPrimitive.Action
160
+ data-slot="alert-dialog-action"
161
+ className={cn(className)}
162
+ {...props}
163
+ />
164
+ </Button>
165
+ )
166
+ }
167
+
168
+ function AlertDialogCancel({
169
+ className,
170
+ variant = 'outline',
171
+ size = 'default',
172
+ ...props
173
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
174
+ Pick<React.ComponentProps<typeof Button>, 'variant' | 'size'>) {
175
+ return (
176
+ <Button variant={variant} size={size} asChild>
177
+ <AlertDialogPrimitive.Cancel
178
+ data-slot="alert-dialog-cancel"
179
+ className={cn(className)}
180
+ {...props}
181
+ />
182
+ </Button>
183
+ )
184
+ }
185
+
186
+ export {
187
+ AlertDialog,
188
+ AlertDialogAction,
189
+ AlertDialogCancel,
190
+ AlertDialogContent,
191
+ AlertDialogDescription,
192
+ AlertDialogFooter,
193
+ AlertDialogHeader,
194
+ AlertDialogMedia,
195
+ AlertDialogOverlay,
196
+ AlertDialogPortal,
197
+ AlertDialogTitle,
198
+ AlertDialogTrigger
199
+ }
@@ -5,13 +5,18 @@ import * as React from 'react'
5
5
  import { cn } from '~/lib/cn'
6
6
 
7
7
  const badgeVariants = cva(
8
- 'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
8
+ 'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[background-color,border-color,color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
9
9
  {
10
10
  variants: {
11
11
  variant: {
12
12
  default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
13
13
  secondary:
14
14
  'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
15
+ success:
16
+ 'border-success/20 bg-success/10 text-success focus-visible:ring-success/20 [a]:hover:bg-success/20',
17
+ warning:
18
+ 'border-warning/20 bg-warning/10 text-warning focus-visible:ring-warning/20 [a]:hover:bg-warning/20',
19
+ info: 'border-info/20 bg-info/10 text-info focus-visible:ring-info/20 [a]:hover:bg-info/20',
15
20
  destructive:
16
21
  'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
17
22
  outline:
@@ -1,3 +1,5 @@
1
+ 'use client'
2
+
1
3
  import { cva, type VariantProps } from 'class-variance-authority'
2
4
  import { Slot } from 'radix-ui'
3
5
  import * as React from 'react'
@@ -32,7 +32,7 @@ function Command({
32
32
 
33
33
  function CommandDialog({
34
34
  title = 'Command Palette',
35
- description = 'Search for a command to run...',
35
+ description = 'Search for a command to run…',
36
36
  children,
37
37
  className,
38
38
  showCloseButton = false,