create-ej-app 25.0.0 → 27.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 (385) hide show
  1. package/README.md +125 -226
  2. package/cli.ts +83 -28
  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 -0
  15. package/template/nextjs/next.config.ts +8 -1
  16. package/template/nextjs/package.json +47 -40
  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 +8 -0
  140. package/template/saas-starter/.env.example +25 -0
  141. package/template/saas-starter/.vscode/extensions.json +11 -0
  142. package/template/saas-starter/.vscode/settings.json +3 -0
  143. package/template/saas-starter/AGENTS.md +302 -0
  144. package/template/saas-starter/CLAUDE.md +1 -0
  145. package/template/saas-starter/Dockerfile +15 -0
  146. package/template/saas-starter/README.md +125 -0
  147. package/template/saas-starter/bunfig.toml +3 -0
  148. package/template/saas-starter/components.json +25 -0
  149. package/template/saas-starter/drizzle.config.ts +10 -0
  150. package/template/saas-starter/eslint.config.mjs +24 -0
  151. package/template/saas-starter/gitignore +14 -0
  152. package/template/saas-starter/next.config.ts +32 -0
  153. package/template/saas-starter/package.json +97 -0
  154. package/template/saas-starter/postcss.config.mjs +7 -0
  155. package/template/saas-starter/prettier.config.mjs +11 -0
  156. package/template/saas-starter/public/logo-dark.svg +4 -0
  157. package/template/saas-starter/public/logo.svg +4 -0
  158. package/template/saas-starter/public/placeholder.svg +1 -0
  159. package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
  160. package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
  161. package/template/saas-starter/src/app/(admin)/actions/users.ts +183 -0
  162. package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
  163. package/template/saas-starter/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
  164. package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
  165. package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
  166. package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
  167. package/template/saas-starter/src/app/(admin)/admin/layout.tsx +53 -0
  168. package/template/saas-starter/src/app/(admin)/admin/page.tsx +60 -0
  169. package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
  170. package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
  171. package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
  172. package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +377 -0
  173. package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
  174. package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
  175. package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
  176. package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
  177. package/template/saas-starter/src/app/(app)/actions/billing.ts +216 -0
  178. package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
  179. package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
  180. package/template/saas-starter/src/app/(app)/actions/members.ts +189 -0
  181. package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
  182. package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
  183. package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +17 -0
  184. package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +31 -0
  185. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +92 -0
  186. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +357 -0
  187. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +130 -0
  188. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +222 -0
  189. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +115 -0
  190. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
  191. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +168 -0
  192. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +604 -0
  193. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +33 -0
  194. package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +55 -0
  195. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
  196. package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
  197. package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +31 -0
  198. package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +26 -0
  199. package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +30 -0
  200. package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
  201. package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +37 -0
  202. package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +200 -0
  203. package/template/saas-starter/src/app/(app)/app/layout.tsx +64 -0
  204. package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +117 -0
  205. package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +40 -0
  206. package/template/saas-starter/src/app/(app)/app/page.tsx +76 -0
  207. package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
  208. package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
  209. package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
  210. package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
  211. package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
  212. package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +131 -0
  213. package/template/saas-starter/src/app/(auth)/login/page.tsx +105 -0
  214. package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +137 -0
  215. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +94 -0
  216. package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +153 -0
  217. package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
  218. package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
  219. package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
  220. package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
  221. package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
  222. package/template/saas-starter/src/app/(marketing)/layout.tsx +48 -0
  223. package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
  224. package/template/saas-starter/src/app/(marketing)/page.tsx +24 -0
  225. package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +63 -0
  226. package/template/saas-starter/src/app/(marketing)/terms/page.tsx +41 -0
  227. package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
  228. package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
  229. package/template/saas-starter/src/app/actions/uploads.ts +36 -0
  230. package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
  231. package/template/saas-starter/src/app/api/auth/[...all]/route.ts +4 -0
  232. package/template/saas-starter/src/app/api/webhooks/stripe/route.ts +94 -0
  233. package/template/saas-starter/src/app/error.tsx +37 -0
  234. package/template/saas-starter/src/app/favicon.ico +0 -0
  235. package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
  236. package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
  237. package/template/saas-starter/src/app/fonts/index.ts +60 -0
  238. package/template/saas-starter/src/app/global-error.tsx +38 -0
  239. package/template/saas-starter/src/app/globals.css +269 -0
  240. package/template/saas-starter/src/app/layout.tsx +77 -0
  241. package/template/saas-starter/src/app/not-found.tsx +49 -0
  242. package/template/saas-starter/src/app/robots.ts +36 -0
  243. package/template/saas-starter/src/app/sitemap.ts +21 -0
  244. package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
  245. package/template/saas-starter/src/components/dashboard-header.tsx +70 -0
  246. package/template/saas-starter/src/components/data-table/enum.ts +5 -0
  247. package/template/saas-starter/src/components/data-table/features.ts +49 -0
  248. package/template/saas-starter/src/components/data-table/index.tsx +961 -0
  249. package/template/saas-starter/src/components/data-table/types.ts +76 -0
  250. package/template/saas-starter/src/components/error.tsx +71 -0
  251. package/template/saas-starter/src/components/file-upload.tsx +254 -0
  252. package/template/saas-starter/src/components/loader.tsx +41 -0
  253. package/template/saas-starter/src/components/logo.tsx +49 -0
  254. package/template/saas-starter/src/components/navigation-search.tsx +140 -0
  255. package/template/saas-starter/src/components/navigation-sidebar.tsx +241 -0
  256. package/template/saas-starter/src/components/page-heading.tsx +38 -0
  257. package/template/saas-starter/src/components/react-query-provider.tsx +51 -0
  258. package/template/saas-starter/src/components/route-error.tsx +55 -0
  259. package/template/saas-starter/src/components/screen-size.tsx +76 -0
  260. package/template/saas-starter/src/components/site-footer.tsx +39 -0
  261. package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
  262. package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
  263. package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
  264. package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
  265. package/template/saas-starter/src/components/theme-provider.tsx +115 -0
  266. package/template/saas-starter/src/components/theme-toggle.tsx +75 -0
  267. package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
  268. package/template/saas-starter/src/components/ui/avatar.tsx +112 -0
  269. package/template/saas-starter/src/components/ui/badge.tsx +54 -0
  270. package/template/saas-starter/src/components/ui/button.tsx +69 -0
  271. package/template/saas-starter/src/components/ui/card.tsx +103 -0
  272. package/template/saas-starter/src/components/ui/checkbox.tsx +32 -0
  273. package/template/saas-starter/src/components/ui/collapsible.tsx +33 -0
  274. package/template/saas-starter/src/components/ui/command.tsx +192 -0
  275. package/template/saas-starter/src/components/ui/dialog.tsx +164 -0
  276. package/template/saas-starter/src/components/ui/dropdown-menu.tsx +273 -0
  277. package/template/saas-starter/src/components/ui/field.tsx +238 -0
  278. package/template/saas-starter/src/components/ui/input-group.tsx +156 -0
  279. package/template/saas-starter/src/components/ui/input.tsx +19 -0
  280. package/template/saas-starter/src/components/ui/label.tsx +24 -0
  281. package/template/saas-starter/src/components/ui/popover.tsx +89 -0
  282. package/template/saas-starter/src/components/ui/select.tsx +195 -0
  283. package/template/saas-starter/src/components/ui/separator.tsx +28 -0
  284. package/template/saas-starter/src/components/ui/sheet.tsx +143 -0
  285. package/template/saas-starter/src/components/ui/sidebar.tsx +702 -0
  286. package/template/saas-starter/src/components/ui/skeleton.tsx +13 -0
  287. package/template/saas-starter/src/components/ui/sonner.tsx +45 -0
  288. package/template/saas-starter/src/components/ui/switch.tsx +33 -0
  289. package/template/saas-starter/src/components/ui/table.tsx +116 -0
  290. package/template/saas-starter/src/components/ui/tabs.tsx +90 -0
  291. package/template/saas-starter/src/components/ui/textarea.tsx +18 -0
  292. package/template/saas-starter/src/components/ui/tooltip.tsx +57 -0
  293. package/template/saas-starter/src/components/user-menu.tsx +213 -0
  294. package/template/saas-starter/src/db/common.ts +63 -0
  295. package/template/saas-starter/src/db/enums.ts +50 -0
  296. package/template/saas-starter/src/db/index.ts +60 -0
  297. package/template/saas-starter/src/db/relations.ts +112 -0
  298. package/template/saas-starter/src/db/schema.ts +342 -0
  299. package/template/saas-starter/src/emails/components/email-layout.tsx +79 -0
  300. package/template/saas-starter/src/emails/invitation.tsx +69 -0
  301. package/template/saas-starter/src/emails/magic-link.tsx +51 -0
  302. package/template/saas-starter/src/emails/test.tsx +34 -0
  303. package/template/saas-starter/src/emails/welcome.tsx +37 -0
  304. package/template/saas-starter/src/env.ts +66 -0
  305. package/template/saas-starter/src/hooks/use-mobile.ts +21 -0
  306. package/template/saas-starter/src/instrumentation.ts +44 -0
  307. package/template/saas-starter/src/lib/actions.ts +55 -0
  308. package/template/saas-starter/src/lib/admin-navigation.ts +63 -0
  309. package/template/saas-starter/src/lib/app-navigation.ts +63 -0
  310. package/template/saas-starter/src/lib/auth-client.ts +34 -0
  311. package/template/saas-starter/src/lib/auth-permissions.ts +61 -0
  312. package/template/saas-starter/src/lib/auth.ts +312 -0
  313. package/template/saas-starter/src/lib/billing.ts +152 -0
  314. package/template/saas-starter/src/lib/callback-url.ts +28 -0
  315. package/template/saas-starter/src/lib/cn.ts +4 -0
  316. package/template/saas-starter/src/lib/constants.ts +91 -0
  317. package/template/saas-starter/src/lib/delay.ts +12 -0
  318. package/template/saas-starter/src/lib/email-service.ts +118 -0
  319. package/template/saas-starter/src/lib/errors.ts +74 -0
  320. package/template/saas-starter/src/lib/format-currency.ts +25 -0
  321. package/template/saas-starter/src/lib/format-date.ts +75 -0
  322. package/template/saas-starter/src/lib/image-url.ts +54 -0
  323. package/template/saas-starter/src/lib/nodemailer.ts +67 -0
  324. package/template/saas-starter/src/lib/organization-access.ts +73 -0
  325. package/template/saas-starter/src/lib/otel.ts +117 -0
  326. package/template/saas-starter/src/lib/plan-limits.ts +37 -0
  327. package/template/saas-starter/src/lib/plans.ts +90 -0
  328. package/template/saas-starter/src/lib/rbac.ts +44 -0
  329. package/template/saas-starter/src/lib/safe-action-client.ts +126 -0
  330. package/template/saas-starter/src/lib/safe-action.ts +187 -0
  331. package/template/saas-starter/src/lib/search.ts +16 -0
  332. package/template/saas-starter/src/lib/seo.ts +7 -0
  333. package/template/saas-starter/src/lib/siteConfig.ts +20 -0
  334. package/template/saas-starter/src/lib/storage.ts +311 -0
  335. package/template/saas-starter/src/lib/stripe.ts +9 -0
  336. package/template/saas-starter/src/lib/toast-message.ts +101 -0
  337. package/template/saas-starter/src/lib/validations.ts +95 -0
  338. package/template/saas-starter/src/proxy.ts +48 -0
  339. package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
  340. package/template/saas-starter/src/scripts/reset-db.ts +22 -0
  341. package/template/saas-starter/src/scripts/seed.ts +85 -0
  342. package/template/saas-starter/src/scripts/setup-db.ts +72 -0
  343. package/template/saas-starter/src/scripts/setup-stripe-plans.ts +206 -0
  344. package/template/saas-starter/src/scripts/test-mail.ts +43 -0
  345. package/template/saas-starter/tests/admin/users.test.ts +64 -0
  346. package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
  347. package/template/saas-starter/tests/db/seed.test.ts +57 -0
  348. package/template/saas-starter/tests/helpers/action.ts +71 -0
  349. package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
  350. package/template/saas-starter/tests/helpers/index.ts +2 -0
  351. package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
  352. package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
  353. package/template/saas-starter/tests/lib/errors.test.ts +10 -0
  354. package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
  355. package/template/saas-starter/tests/lib/search.test.ts +23 -0
  356. package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
  357. package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
  358. package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
  359. package/template/saas-starter/tests/setup/preload.ts +108 -0
  360. package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
  361. package/template/saas-starter/tsconfig.json +52 -0
  362. package/cli.js +0 -143
  363. package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
  364. package/template/nextjs/src/app/(app)/layout.tsx +0 -20
  365. package/template/nextjs/terraform/artifact-registry.tf +0 -5
  366. package/template/nextjs/terraform/backend-service.tf +0 -13
  367. package/template/nextjs/terraform/cleanup.sh +0 -9
  368. package/template/nextjs/terraform/cloudbuild.tf +0 -70
  369. package/template/nextjs/terraform/cloudrun.tf +0 -68
  370. package/template/nextjs/terraform/database.tf +0 -60
  371. package/template/nextjs/terraform/forwarding-rule.tf +0 -43
  372. package/template/nextjs/terraform/gitignore +0 -4
  373. package/template/nextjs/terraform/ip-address.tf +0 -11
  374. package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
  375. package/template/nextjs/terraform/network.tf +0 -21
  376. package/template/nextjs/terraform/pre.sh +0 -3
  377. package/template/nextjs/terraform/providers.tf +0 -18
  378. package/template/nextjs/terraform/secret.txt +0 -11
  379. package/template/nextjs/terraform/setup.sh +0 -25
  380. package/template/nextjs/terraform/ssl-certs.tf +0 -32
  381. package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
  382. package/template/nextjs/terraform/target-https-proxy.tf +0 -14
  383. package/template/nextjs/terraform/terraform.tfvars +0 -4
  384. package/template/nextjs/terraform/url-map.tf +0 -23
  385. package/template/nextjs/terraform/variables.tf +0 -21
@@ -0,0 +1,702 @@
1
+ 'use client'
2
+
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { Slot } from 'radix-ui'
5
+ import * as React from 'react'
6
+
7
+ import { PanelLeftIcon } from 'lucide-react'
8
+ import { Button } from '~/components/ui/button'
9
+ import { Input } from '~/components/ui/input'
10
+ import { Separator } from '~/components/ui/separator'
11
+ import {
12
+ Sheet,
13
+ SheetContent,
14
+ SheetDescription,
15
+ SheetHeader,
16
+ SheetTitle
17
+ } from '~/components/ui/sheet'
18
+ import { Skeleton } from '~/components/ui/skeleton'
19
+ import {
20
+ Tooltip,
21
+ TooltipContent,
22
+ TooltipTrigger
23
+ } from '~/components/ui/tooltip'
24
+ import { useIsMobile } from '~/hooks/use-mobile'
25
+ import { cn } from '~/lib/cn'
26
+
27
+ const SIDEBAR_COOKIE_NAME = 'sidebar_state'
28
+ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
29
+ const SIDEBAR_WIDTH = '16rem'
30
+ const SIDEBAR_WIDTH_MOBILE = '18rem'
31
+ const SIDEBAR_WIDTH_ICON = '3rem'
32
+ const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
33
+
34
+ type SidebarContextProps = {
35
+ state: 'expanded' | 'collapsed'
36
+ open: boolean
37
+ setOpen: (open: boolean) => void
38
+ openMobile: boolean
39
+ setOpenMobile: (open: boolean) => void
40
+ isMobile: boolean
41
+ toggleSidebar: () => void
42
+ }
43
+
44
+ const SidebarContext = React.createContext<SidebarContextProps | null>(null)
45
+
46
+ function useSidebar() {
47
+ const context = React.useContext(SidebarContext)
48
+ if (!context) {
49
+ throw new Error('useSidebar must be used within a SidebarProvider.')
50
+ }
51
+
52
+ return context
53
+ }
54
+
55
+ function SidebarProvider({
56
+ defaultOpen = true,
57
+ open: openProp,
58
+ onOpenChange: setOpenProp,
59
+ className,
60
+ style,
61
+ children,
62
+ ...props
63
+ }: React.ComponentProps<'div'> & {
64
+ defaultOpen?: boolean
65
+ open?: boolean
66
+ onOpenChange?: (open: boolean) => void
67
+ }) {
68
+ const isMobile = useIsMobile()
69
+ const [openMobile, setOpenMobile] = React.useState(false)
70
+
71
+ // This is the internal state of the sidebar.
72
+ // We use openProp and setOpenProp for control from outside the component.
73
+ const [_open, _setOpen] = React.useState(defaultOpen)
74
+ const open = openProp ?? _open
75
+ const setOpen = React.useCallback(
76
+ (value: boolean | ((value: boolean) => boolean)) => {
77
+ const openState = typeof value === 'function' ? value(open) : value
78
+ if (setOpenProp) {
79
+ setOpenProp(openState)
80
+ } else {
81
+ _setOpen(openState)
82
+ }
83
+
84
+ // This sets the cookie to keep the sidebar state.
85
+ document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
86
+ },
87
+ [setOpenProp, open]
88
+ )
89
+
90
+ // Helper to toggle the sidebar.
91
+ const toggleSidebar = React.useCallback(() => {
92
+ return isMobile ? setOpenMobile(open => !open) : setOpen(open => !open)
93
+ }, [isMobile, setOpen, setOpenMobile])
94
+
95
+ // Adds a keyboard shortcut to toggle the sidebar.
96
+ React.useEffect(() => {
97
+ const handleKeyDown = (event: KeyboardEvent) => {
98
+ if (
99
+ event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
100
+ (event.metaKey || event.ctrlKey)
101
+ ) {
102
+ event.preventDefault()
103
+ toggleSidebar()
104
+ }
105
+ }
106
+
107
+ window.addEventListener('keydown', handleKeyDown)
108
+ return () => window.removeEventListener('keydown', handleKeyDown)
109
+ }, [toggleSidebar])
110
+
111
+ // We add a state so that we can do data-state="expanded" or "collapsed".
112
+ // This makes it easier to style the sidebar with Tailwind classes.
113
+ const state = open ? 'expanded' : 'collapsed'
114
+
115
+ const contextValue = React.useMemo<SidebarContextProps>(
116
+ () => ({
117
+ state,
118
+ open,
119
+ setOpen,
120
+ isMobile,
121
+ openMobile,
122
+ setOpenMobile,
123
+ toggleSidebar
124
+ }),
125
+ [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
126
+ )
127
+
128
+ return (
129
+ <SidebarContext.Provider value={contextValue}>
130
+ <div
131
+ data-slot="sidebar-wrapper"
132
+ style={
133
+ {
134
+ '--sidebar-width': SIDEBAR_WIDTH,
135
+ '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
136
+ ...style
137
+ } as React.CSSProperties
138
+ }
139
+ className={cn(
140
+ 'group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full',
141
+ className
142
+ )}
143
+ {...props}
144
+ >
145
+ {children}
146
+ </div>
147
+ </SidebarContext.Provider>
148
+ )
149
+ }
150
+
151
+ function Sidebar({
152
+ side = 'left',
153
+ variant = 'sidebar',
154
+ collapsible = 'offcanvas',
155
+ className,
156
+ children,
157
+ dir,
158
+ ...props
159
+ }: React.ComponentProps<'div'> & {
160
+ side?: 'left' | 'right'
161
+ variant?: 'sidebar' | 'floating' | 'inset'
162
+ collapsible?: 'offcanvas' | 'icon' | 'none'
163
+ }) {
164
+ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
165
+
166
+ if (collapsible === 'none') {
167
+ return (
168
+ <div
169
+ data-slot="sidebar"
170
+ className={cn(
171
+ 'bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col',
172
+ className
173
+ )}
174
+ {...props}
175
+ >
176
+ {children}
177
+ </div>
178
+ )
179
+ }
180
+
181
+ if (isMobile) {
182
+ return (
183
+ <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
184
+ <SheetContent
185
+ dir={dir}
186
+ data-sidebar="sidebar"
187
+ data-slot="sidebar"
188
+ data-mobile="true"
189
+ className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
190
+ style={
191
+ {
192
+ '--sidebar-width': SIDEBAR_WIDTH_MOBILE
193
+ } as React.CSSProperties
194
+ }
195
+ side={side}
196
+ >
197
+ <SheetHeader className="sr-only">
198
+ <SheetTitle>Sidebar</SheetTitle>
199
+ <SheetDescription>Displays the mobile sidebar.</SheetDescription>
200
+ </SheetHeader>
201
+ <div className="flex h-full w-full flex-col">{children}</div>
202
+ </SheetContent>
203
+ </Sheet>
204
+ )
205
+ }
206
+
207
+ return (
208
+ <div
209
+ className="group peer text-sidebar-foreground hidden md:block"
210
+ data-state={state}
211
+ data-collapsible={state === 'collapsed' ? collapsible : ''}
212
+ data-variant={variant}
213
+ data-side={side}
214
+ data-slot="sidebar"
215
+ >
216
+ {/* This is what handles the sidebar gap on desktop */}
217
+ <div
218
+ data-slot="sidebar-gap"
219
+ className={cn(
220
+ 'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
221
+ 'group-data-[collapsible=offcanvas]:w-0',
222
+ 'group-data-[side=right]:rotate-180',
223
+ variant === 'floating' || variant === 'inset'
224
+ ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
225
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)'
226
+ )}
227
+ />
228
+ <div
229
+ data-slot="sidebar-container"
230
+ data-side={side}
231
+ className={cn(
232
+ 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex',
233
+ // Adjust the padding for floating and inset variants.
234
+ variant === 'floating' || variant === 'inset'
235
+ ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
236
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
237
+ className
238
+ )}
239
+ {...props}
240
+ >
241
+ <div
242
+ data-sidebar="sidebar"
243
+ data-slot="sidebar-inner"
244
+ className="bg-sidebar group-data-[variant=floating]:ring-sidebar-border flex size-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1"
245
+ >
246
+ {children}
247
+ </div>
248
+ </div>
249
+ </div>
250
+ )
251
+ }
252
+
253
+ function SidebarTrigger({
254
+ className,
255
+ onClick,
256
+ ...props
257
+ }: React.ComponentProps<typeof Button>) {
258
+ const { toggleSidebar } = useSidebar()
259
+
260
+ return (
261
+ <Button
262
+ data-sidebar="trigger"
263
+ data-slot="sidebar-trigger"
264
+ variant="ghost"
265
+ size="icon-sm"
266
+ className={cn(className)}
267
+ onClick={event => {
268
+ onClick?.(event)
269
+ toggleSidebar()
270
+ }}
271
+ {...props}
272
+ >
273
+ <PanelLeftIcon />
274
+ <span className="sr-only">Toggle Sidebar</span>
275
+ </Button>
276
+ )
277
+ }
278
+
279
+ function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) {
280
+ const { toggleSidebar } = useSidebar()
281
+
282
+ return (
283
+ <button
284
+ data-sidebar="rail"
285
+ data-slot="sidebar-rail"
286
+ aria-label="Toggle Sidebar"
287
+ tabIndex={-1}
288
+ onClick={toggleSidebar}
289
+ title="Toggle Sidebar"
290
+ className={cn(
291
+ 'hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2',
292
+ 'in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize',
293
+ '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
294
+ 'hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full',
295
+ '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
296
+ '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
297
+ className
298
+ )}
299
+ {...props}
300
+ />
301
+ )
302
+ }
303
+
304
+ function SidebarInset({ className, ...props }: React.ComponentProps<'main'>) {
305
+ return (
306
+ <main
307
+ data-slot="sidebar-inset"
308
+ className={cn(
309
+ 'bg-background relative flex w-full flex-1 flex-col md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
310
+ className
311
+ )}
312
+ {...props}
313
+ />
314
+ )
315
+ }
316
+
317
+ function SidebarInput({
318
+ className,
319
+ ...props
320
+ }: React.ComponentProps<typeof Input>) {
321
+ return (
322
+ <Input
323
+ data-slot="sidebar-input"
324
+ data-sidebar="input"
325
+ className={cn('bg-background h-8 w-full shadow-none', className)}
326
+ {...props}
327
+ />
328
+ )
329
+ }
330
+
331
+ function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) {
332
+ return (
333
+ <div
334
+ data-slot="sidebar-header"
335
+ data-sidebar="header"
336
+ className={cn('flex flex-col gap-2 p-2', className)}
337
+ {...props}
338
+ />
339
+ )
340
+ }
341
+
342
+ function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) {
343
+ return (
344
+ <div
345
+ data-slot="sidebar-footer"
346
+ data-sidebar="footer"
347
+ className={cn('flex flex-col gap-2 p-2', className)}
348
+ {...props}
349
+ />
350
+ )
351
+ }
352
+
353
+ function SidebarSeparator({
354
+ className,
355
+ ...props
356
+ }: React.ComponentProps<typeof Separator>) {
357
+ return (
358
+ <Separator
359
+ data-slot="sidebar-separator"
360
+ data-sidebar="separator"
361
+ className={cn('bg-sidebar-border mx-2 w-auto', className)}
362
+ {...props}
363
+ />
364
+ )
365
+ }
366
+
367
+ function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) {
368
+ return (
369
+ <div
370
+ data-slot="sidebar-content"
371
+ data-sidebar="content"
372
+ className={cn(
373
+ 'no-scrollbar flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
374
+ className
375
+ )}
376
+ {...props}
377
+ />
378
+ )
379
+ }
380
+
381
+ function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) {
382
+ return (
383
+ <div
384
+ data-slot="sidebar-group"
385
+ data-sidebar="group"
386
+ className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
387
+ {...props}
388
+ />
389
+ )
390
+ }
391
+
392
+ function SidebarGroupLabel({
393
+ className,
394
+ asChild = false,
395
+ ...props
396
+ }: React.ComponentProps<'div'> & { asChild?: boolean }) {
397
+ const Comp = asChild ? Slot.Root : 'div'
398
+
399
+ return (
400
+ <Comp
401
+ data-slot="sidebar-group-label"
402
+ data-sidebar="group-label"
403
+ className={cn(
404
+ 'text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
405
+ className
406
+ )}
407
+ {...props}
408
+ />
409
+ )
410
+ }
411
+
412
+ function SidebarGroupAction({
413
+ className,
414
+ asChild = false,
415
+ ...props
416
+ }: React.ComponentProps<'button'> & { asChild?: boolean }) {
417
+ const Comp = asChild ? Slot.Root : 'button'
418
+
419
+ return (
420
+ <Comp
421
+ data-slot="sidebar-group-action"
422
+ data-sidebar="group-action"
423
+ className={cn(
424
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
425
+ className
426
+ )}
427
+ {...props}
428
+ />
429
+ )
430
+ }
431
+
432
+ function SidebarGroupContent({
433
+ className,
434
+ ...props
435
+ }: React.ComponentProps<'div'>) {
436
+ return (
437
+ <div
438
+ data-slot="sidebar-group-content"
439
+ data-sidebar="group-content"
440
+ className={cn('w-full text-sm', className)}
441
+ {...props}
442
+ />
443
+ )
444
+ }
445
+
446
+ function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
447
+ return (
448
+ <ul
449
+ data-slot="sidebar-menu"
450
+ data-sidebar="menu"
451
+ className={cn('flex w-full min-w-0 flex-col gap-1', className)}
452
+ {...props}
453
+ />
454
+ )
455
+ }
456
+
457
+ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) {
458
+ return (
459
+ <li
460
+ data-slot="sidebar-menu-item"
461
+ data-sidebar="menu-item"
462
+ className={cn('group/menu-item relative', className)}
463
+ {...props}
464
+ />
465
+ )
466
+ }
467
+
468
+ const sidebarMenuButtonVariants = cva(
469
+ 'peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate',
470
+ {
471
+ variants: {
472
+ variant: {
473
+ default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
474
+ outline:
475
+ 'bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]'
476
+ },
477
+ size: {
478
+ default: 'h-8 text-sm',
479
+ sm: 'h-7 text-xs',
480
+ lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!'
481
+ }
482
+ },
483
+ defaultVariants: {
484
+ variant: 'default',
485
+ size: 'default'
486
+ }
487
+ }
488
+ )
489
+
490
+ function SidebarMenuButton({
491
+ asChild = false,
492
+ isActive = false,
493
+ variant = 'default',
494
+ size = 'default',
495
+ tooltip,
496
+ className,
497
+ ...props
498
+ }: React.ComponentProps<'button'> & {
499
+ asChild?: boolean
500
+ isActive?: boolean
501
+ tooltip?: string | React.ComponentProps<typeof TooltipContent>
502
+ } & VariantProps<typeof sidebarMenuButtonVariants>) {
503
+ const Comp = asChild ? Slot.Root : 'button'
504
+ const { isMobile, state } = useSidebar()
505
+
506
+ const button = (
507
+ <Comp
508
+ data-slot="sidebar-menu-button"
509
+ data-sidebar="menu-button"
510
+ data-size={size}
511
+ data-active={isActive}
512
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
513
+ {...props}
514
+ />
515
+ )
516
+
517
+ if (!tooltip) {
518
+ return button
519
+ }
520
+
521
+ if (typeof tooltip === 'string') {
522
+ tooltip = {
523
+ children: tooltip
524
+ }
525
+ }
526
+
527
+ return (
528
+ <Tooltip>
529
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
530
+ <TooltipContent
531
+ side="right"
532
+ align="center"
533
+ hidden={state !== 'collapsed' || isMobile}
534
+ {...tooltip}
535
+ />
536
+ </Tooltip>
537
+ )
538
+ }
539
+
540
+ function SidebarMenuAction({
541
+ className,
542
+ asChild = false,
543
+ showOnHover = false,
544
+ ...props
545
+ }: React.ComponentProps<'button'> & {
546
+ asChild?: boolean
547
+ showOnHover?: boolean
548
+ }) {
549
+ const Comp = asChild ? Slot.Root : 'button'
550
+
551
+ return (
552
+ <Comp
553
+ data-slot="sidebar-menu-action"
554
+ data-sidebar="menu-action"
555
+ className={cn(
556
+ 'text-sidebar-foreground ring-sidebar-ring peer-hover/menu-button:text-sidebar-accent-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0',
557
+ showOnHover &&
558
+ 'peer-data-active/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 aria-expanded:opacity-100 md:opacity-0',
559
+ className
560
+ )}
561
+ {...props}
562
+ />
563
+ )
564
+ }
565
+
566
+ function SidebarMenuBadge({
567
+ className,
568
+ ...props
569
+ }: React.ComponentProps<'div'>) {
570
+ return (
571
+ <div
572
+ data-slot="sidebar-menu-badge"
573
+ data-sidebar="menu-badge"
574
+ className={cn(
575
+ 'text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none group-data-[collapsible=icon]:hidden peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1',
576
+ className
577
+ )}
578
+ {...props}
579
+ />
580
+ )
581
+ }
582
+
583
+ function SidebarMenuSkeleton({
584
+ className,
585
+ showIcon = false,
586
+ ...props
587
+ }: React.ComponentProps<'div'> & {
588
+ showIcon?: boolean
589
+ }) {
590
+ // Random width between 50 to 90%.
591
+ const [width] = React.useState(() => {
592
+ return `${Math.floor(Math.random() * 40) + 50}%`
593
+ })
594
+
595
+ return (
596
+ <div
597
+ data-slot="sidebar-menu-skeleton"
598
+ data-sidebar="menu-skeleton"
599
+ className={cn('flex h-8 items-center gap-2 rounded-md px-2', className)}
600
+ {...props}
601
+ >
602
+ {showIcon && (
603
+ <Skeleton
604
+ className="size-4 rounded-md"
605
+ data-sidebar="menu-skeleton-icon"
606
+ />
607
+ )}
608
+ <Skeleton
609
+ className="h-4 max-w-(--skeleton-width) flex-1"
610
+ data-sidebar="menu-skeleton-text"
611
+ style={
612
+ {
613
+ '--skeleton-width': width
614
+ } as React.CSSProperties
615
+ }
616
+ />
617
+ </div>
618
+ )
619
+ }
620
+
621
+ function SidebarMenuSub({ className, ...props }: React.ComponentProps<'ul'>) {
622
+ return (
623
+ <ul
624
+ data-slot="sidebar-menu-sub"
625
+ data-sidebar="menu-sub"
626
+ className={cn(
627
+ 'border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5 group-data-[collapsible=icon]:hidden',
628
+ className
629
+ )}
630
+ {...props}
631
+ />
632
+ )
633
+ }
634
+
635
+ function SidebarMenuSubItem({
636
+ className,
637
+ ...props
638
+ }: React.ComponentProps<'li'>) {
639
+ return (
640
+ <li
641
+ data-slot="sidebar-menu-sub-item"
642
+ data-sidebar="menu-sub-item"
643
+ className={cn('group/menu-sub-item relative', className)}
644
+ {...props}
645
+ />
646
+ )
647
+ }
648
+
649
+ function SidebarMenuSubButton({
650
+ asChild = false,
651
+ size = 'md',
652
+ isActive = false,
653
+ className,
654
+ ...props
655
+ }: React.ComponentProps<'a'> & {
656
+ asChild?: boolean
657
+ size?: 'sm' | 'md'
658
+ isActive?: boolean
659
+ }) {
660
+ const Comp = asChild ? Slot.Root : 'a'
661
+
662
+ return (
663
+ <Comp
664
+ data-slot="sidebar-menu-sub-button"
665
+ data-sidebar="menu-sub-button"
666
+ data-size={size}
667
+ data-active={isActive}
668
+ className={cn(
669
+ 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden group-data-[collapsible=icon]:hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
670
+ className
671
+ )}
672
+ {...props}
673
+ />
674
+ )
675
+ }
676
+
677
+ export {
678
+ Sidebar,
679
+ SidebarContent,
680
+ SidebarFooter,
681
+ SidebarGroup,
682
+ SidebarGroupAction,
683
+ SidebarGroupContent,
684
+ SidebarGroupLabel,
685
+ SidebarHeader,
686
+ SidebarInput,
687
+ SidebarInset,
688
+ SidebarMenu,
689
+ SidebarMenuAction,
690
+ SidebarMenuBadge,
691
+ SidebarMenuButton,
692
+ SidebarMenuItem,
693
+ SidebarMenuSkeleton,
694
+ SidebarMenuSub,
695
+ SidebarMenuSubButton,
696
+ SidebarMenuSubItem,
697
+ SidebarProvider,
698
+ SidebarRail,
699
+ SidebarSeparator,
700
+ SidebarTrigger,
701
+ useSidebar
702
+ }
@@ -0,0 +1,13 @@
1
+ import { cn } from '~/lib/cn'
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn('bg-muted animate-pulse rounded-md', className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }