create-nexst-monorepo 0.1.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 (303) hide show
  1. package/dist/index.js +1727 -0
  2. package/package.json +37 -0
  3. package/template/.agents/plugins/marketplace.json +21 -0
  4. package/template/.claude/settings.json +6 -0
  5. package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
  6. package/template/.claude/skills/frontend-design/SKILL.md +154 -0
  7. package/template/.claude/skills/theme-factory/SKILL.md +95 -0
  8. package/template/.claude/skills/theme-factory/references/schema.md +104 -0
  9. package/template/.claude/skills/theme-factory/scripts/theme.mjs +261 -0
  10. package/template/.dockerignore +43 -0
  11. package/template/.editorconfig +10 -0
  12. package/template/.github/workflows/ci.yaml +289 -0
  13. package/template/.github/workflows/publish-app.yaml +66 -0
  14. package/template/.github/workflows/publish.yaml +148 -0
  15. package/template/.github/workflows/snyk.yaml +34 -0
  16. package/template/.vscode/settings.json +8 -0
  17. package/template/AGENTS.md +42 -0
  18. package/template/Dockerfile.base +9 -0
  19. package/template/README.md +54 -0
  20. package/template/_gitignore +10 -0
  21. package/template/apps/api/.env.example +32 -0
  22. package/template/apps/api/AGENTS.md +44 -0
  23. package/template/apps/api/Dockerfile +69 -0
  24. package/template/apps/api/_gitignore +8 -0
  25. package/template/apps/api/knip.config.ts +22 -0
  26. package/template/apps/api/kustomize/base/application.properties +16 -0
  27. package/template/apps/api/kustomize/base/deployment.yaml +94 -0
  28. package/template/apps/api/kustomize/base/kustomization.yaml +17 -0
  29. package/template/apps/api/kustomize/base/service.yaml +10 -0
  30. package/template/apps/api/kustomize/overlays/prod/kustomization.yaml +17 -0
  31. package/template/apps/api/kustomize/overlays/staging/application.properties +1 -0
  32. package/template/apps/api/kustomize/overlays/staging/kustomization.yaml +21 -0
  33. package/template/apps/api/kustomize/secret.keys +14 -0
  34. package/template/apps/api/oxfmt.config.ts +8 -0
  35. package/template/apps/api/oxlint.config.ts +107 -0
  36. package/template/apps/api/package.json +79 -0
  37. package/template/apps/api/prisma/migrations/20260927000000_init/migration.sql +113 -0
  38. package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
  39. package/template/apps/api/prisma/schema.prisma +113 -0
  40. package/template/apps/api/prisma.config.ts +7 -0
  41. package/template/apps/api/scripts/auth.cli.ts +19 -0
  42. package/template/apps/api/scripts/seal-secret.sh +171 -0
  43. package/template/apps/api/sonar-project.properties +9 -0
  44. package/template/apps/api/src/app-info/app-info.dto.ts +10 -0
  45. package/template/apps/api/src/app-info/app-info.module.ts +7 -0
  46. package/template/apps/api/src/app-info/app-info.resolver.spec.ts +14 -0
  47. package/template/apps/api/src/app-info/app-info.resolver.ts +21 -0
  48. package/template/apps/api/src/app.module.spec.ts +93 -0
  49. package/template/apps/api/src/app.module.ts +77 -0
  50. package/template/apps/api/src/auth/auth.guard.spec.ts +90 -0
  51. package/template/apps/api/src/auth/auth.guard.ts +97 -0
  52. package/template/apps/api/src/auth/auth.ts +129 -0
  53. package/template/apps/api/src/auth/decorator/allow-anonymous.decorator.ts +17 -0
  54. package/template/apps/api/src/auth/decorator/is-email-verified.decorator.spec.ts +41 -0
  55. package/template/apps/api/src/auth/decorator/is-email-verified.decorator.ts +20 -0
  56. package/template/apps/api/src/auth/decorator/roles.decorator.ts +27 -0
  57. package/template/apps/api/src/auth/testing/make-session.ts +31 -0
  58. package/template/apps/api/src/auth/types/auth.types.ts +27 -0
  59. package/template/apps/api/src/bootstrap/register-graphql-enums.ts +12 -0
  60. package/template/apps/api/src/bootstrap/security.ts +24 -0
  61. package/template/apps/api/src/bootstrap/swagger.ts +28 -0
  62. package/template/apps/api/src/config/app.config.spec.ts +22 -0
  63. package/template/apps/api/src/config/app.config.ts +122 -0
  64. package/template/apps/api/src/config/graphql-depth-limit.ts +103 -0
  65. package/template/apps/api/src/config/graphql.config.ts +77 -0
  66. package/template/apps/api/src/config/resources/app.yaml +29 -0
  67. package/template/apps/api/src/errors/app-parse-uuid.pipe.ts +27 -0
  68. package/template/apps/api/src/errors/app.exception.ts +47 -0
  69. package/template/apps/api/src/errors/error-code.enum.ts +54 -0
  70. package/template/apps/api/src/errors/http-status-fallback-code.util.ts +27 -0
  71. package/template/apps/api/src/errors/validation-exception.factory.ts +43 -0
  72. package/template/apps/api/src/health/health.controller.spec.ts +82 -0
  73. package/template/apps/api/src/health/health.controller.ts +57 -0
  74. package/template/apps/api/src/health/health.module.ts +12 -0
  75. package/template/apps/api/src/logging/context-logger.ts +18 -0
  76. package/template/apps/api/src/logging/logger.config.ts +23 -0
  77. package/template/apps/api/src/logging/logging.module.ts +22 -0
  78. package/template/apps/api/src/mail/mail.module.ts +12 -0
  79. package/template/apps/api/src/mail/mail.service.spec.ts +47 -0
  80. package/template/apps/api/src/mail/mail.service.ts +123 -0
  81. package/template/apps/api/src/main.ts +41 -0
  82. package/template/apps/api/src/prisma/prisma.module.ts +12 -0
  83. package/template/apps/api/src/prisma/prisma.service.spec.ts +136 -0
  84. package/template/apps/api/src/prisma/prisma.service.ts +138 -0
  85. package/template/apps/api/src/queue/dto/enqueue-example.input.ts +11 -0
  86. package/template/apps/api/src/queue/example-queue.resolver.ts +14 -0
  87. package/template/apps/api/src/queue/example-queue.service.spec.ts +21 -0
  88. package/template/apps/api/src/queue/example-queue.service.ts +32 -0
  89. package/template/apps/api/src/queue/queue.module.ts +27 -0
  90. package/template/apps/api/src/r2/r2.module.ts +12 -0
  91. package/template/apps/api/src/r2/r2.service.spec.ts +117 -0
  92. package/template/apps/api/src/r2/r2.service.ts +131 -0
  93. package/template/apps/api/src/user/dto/user.dto.ts +26 -0
  94. package/template/apps/api/src/user/user.module.ts +10 -0
  95. package/template/apps/api/src/user/user.resolver.spec.ts +18 -0
  96. package/template/apps/api/src/user/user.resolver.ts +24 -0
  97. package/template/apps/api/src/utils/catch-everything-exception.filter.spec.ts +292 -0
  98. package/template/apps/api/src/utils/catch-everything-exception.filter.ts +70 -0
  99. package/template/apps/api/src/utils/error-handler.util.spec.ts +20 -0
  100. package/template/apps/api/src/utils/error-handler.util.ts +12 -0
  101. package/template/apps/api/src/utils/escape-like-pattern.util.spec.ts +29 -0
  102. package/template/apps/api/src/utils/escape-like-pattern.util.ts +11 -0
  103. package/template/apps/api/src/utils/pagination.input.ts +20 -0
  104. package/template/apps/api/src/utils/prisma-exception.filter.spec.ts +186 -0
  105. package/template/apps/api/src/utils/prisma-exception.filter.ts +45 -0
  106. package/template/apps/api/src/utils/prisma-graphql-exception.filter.spec.ts +216 -0
  107. package/template/apps/api/src/utils/prisma-graphql-exception.filter.ts +43 -0
  108. package/template/apps/api/tests/app.e2e-spec.ts +35 -0
  109. package/template/apps/api/tests/auth.e2e-spec.ts +38 -0
  110. package/template/apps/api/tests/helpers/api.ts +27 -0
  111. package/template/apps/api/tsconfig.json +11 -0
  112. package/template/apps/api/vitest.config.e2e.ts +16 -0
  113. package/template/apps/api/vitest.config.ts +40 -0
  114. package/template/apps/web/.env.example +7 -0
  115. package/template/apps/web/.storybook/decorators.tsx +38 -0
  116. package/template/apps/web/.storybook/main.ts +17 -0
  117. package/template/apps/web/.storybook/preview.ts +38 -0
  118. package/template/apps/web/.storybook/vitest.setup.ts +5 -0
  119. package/template/apps/web/AGENTS.md +29 -0
  120. package/template/apps/web/Dockerfile +49 -0
  121. package/template/apps/web/_gitignore +16 -0
  122. package/template/apps/web/knip.config.ts +15 -0
  123. package/template/apps/web/kustomize/base/application.properties +5 -0
  124. package/template/apps/web/kustomize/base/deployment.yaml +60 -0
  125. package/template/apps/web/kustomize/base/kustomization.yaml +17 -0
  126. package/template/apps/web/kustomize/base/service.yaml +10 -0
  127. package/template/apps/web/kustomize/overlays/prod/kustomization.yaml +12 -0
  128. package/template/apps/web/kustomize/overlays/staging/application.properties +2 -0
  129. package/template/apps/web/kustomize/overlays/staging/kustomization.yaml +18 -0
  130. package/template/apps/web/messages/de.json +124 -0
  131. package/template/apps/web/messages/en.json +124 -0
  132. package/template/apps/web/next-env.d.ts +7 -0
  133. package/template/apps/web/next.config.ts +39 -0
  134. package/template/apps/web/oxfmt.config.ts +9 -0
  135. package/template/apps/web/oxlint.config.ts +36 -0
  136. package/template/apps/web/package.json +80 -0
  137. package/template/apps/web/playwright.config.ts +34 -0
  138. package/template/apps/web/postcss.config.mjs +7 -0
  139. package/template/apps/web/sonar-project.properties +9 -0
  140. package/template/apps/web/src/app/[locale]/(auth)/recovery/page.tsx +22 -0
  141. package/template/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +29 -0
  142. package/template/apps/web/src/app/[locale]/(auth)/sign-in/page.tsx +24 -0
  143. package/template/apps/web/src/app/[locale]/(auth)/sign-up/page.tsx +24 -0
  144. package/template/apps/web/src/app/[locale]/(auth)/verify/page.tsx +24 -0
  145. package/template/apps/web/src/app/[locale]/dashboard/page.tsx +34 -0
  146. package/template/apps/web/src/app/[locale]/layout.tsx +60 -0
  147. package/template/apps/web/src/app/[locale]/page.tsx +23 -0
  148. package/template/apps/web/src/app/api/health/route.ts +3 -0
  149. package/template/apps/web/src/app/global-error.tsx +19 -0
  150. package/template/apps/web/src/app/globals.css +98 -0
  151. package/template/apps/web/src/app/icon.svg +1 -0
  152. package/template/apps/web/src/app/robots.ts +15 -0
  153. package/template/apps/web/src/app/sitemap.ts +24 -0
  154. package/template/apps/web/src/components/alert/alert.stories.tsx +25 -0
  155. package/template/apps/web/src/components/alert/alert.tsx +50 -0
  156. package/template/apps/web/src/components/avatar/avatar.stories.tsx +116 -0
  157. package/template/apps/web/src/components/avatar/avatar.tsx +88 -0
  158. package/template/apps/web/src/components/button/button-variants.ts +28 -0
  159. package/template/apps/web/src/components/button/button.stories.tsx +33 -0
  160. package/template/apps/web/src/components/button/button.tsx +32 -0
  161. package/template/apps/web/src/components/checkbox/checkbox.stories.tsx +312 -0
  162. package/template/apps/web/src/components/checkbox/checkbox.tsx +38 -0
  163. package/template/apps/web/src/components/dialog/danger-panel.stories.tsx +41 -0
  164. package/template/apps/web/src/components/dialog/danger-panel.tsx +32 -0
  165. package/template/apps/web/src/components/dialog/dialog.stories.tsx +239 -0
  166. package/template/apps/web/src/components/dialog/dialog.tsx +136 -0
  167. package/template/apps/web/src/components/dialog-buttons.stories.tsx +55 -0
  168. package/template/apps/web/src/components/dialog-buttons.tsx +48 -0
  169. package/template/apps/web/src/components/drawer/drawer.stories.tsx +251 -0
  170. package/template/apps/web/src/components/drawer/drawer.tsx +120 -0
  171. package/template/apps/web/src/components/dropdown-menu/dropdown-menu.stories.tsx +329 -0
  172. package/template/apps/web/src/components/dropdown-menu/dropdown-menu.tsx +263 -0
  173. package/template/apps/web/src/components/empty-state.stories.tsx +49 -0
  174. package/template/apps/web/src/components/empty-state.tsx +33 -0
  175. package/template/apps/web/src/components/field-group.stories.tsx +47 -0
  176. package/template/apps/web/src/components/field-group.tsx +20 -0
  177. package/template/apps/web/src/components/field-label.stories.tsx +34 -0
  178. package/template/apps/web/src/components/field-label.tsx +28 -0
  179. package/template/apps/web/src/components/icons.tsx +25 -0
  180. package/template/apps/web/src/components/input/input-otp.stories.tsx +86 -0
  181. package/template/apps/web/src/components/input/input-otp.tsx +184 -0
  182. package/template/apps/web/src/components/input/input.stories.tsx +35 -0
  183. package/template/apps/web/src/components/input/input.tsx +180 -0
  184. package/template/apps/web/src/components/popover/popover.stories.tsx +479 -0
  185. package/template/apps/web/src/components/popover/popover.tsx +45 -0
  186. package/template/apps/web/src/components/responsive-dialog/responsive-dialog.stories.tsx +307 -0
  187. package/template/apps/web/src/components/responsive-dialog/responsive-dialog.tsx +96 -0
  188. package/template/apps/web/src/components/searchbar/searchbar.stories.tsx +59 -0
  189. package/template/apps/web/src/components/searchbar/searchbar.tsx +64 -0
  190. package/template/apps/web/src/components/select/select.stories.tsx +293 -0
  191. package/template/apps/web/src/components/select/select.tsx +445 -0
  192. package/template/apps/web/src/components/skeleton/skeleton.stories.tsx +76 -0
  193. package/template/apps/web/src/components/skeleton/skeleton.tsx +13 -0
  194. package/template/apps/web/src/components/spinner/spinner.tsx +18 -0
  195. package/template/apps/web/src/components/switch/switch.stories.tsx +71 -0
  196. package/template/apps/web/src/components/switch/switch.tsx +35 -0
  197. package/template/apps/web/src/components/tabs/tabs.stories.tsx +209 -0
  198. package/template/apps/web/src/components/tabs/tabs.tsx +69 -0
  199. package/template/apps/web/src/components/tabs/url-tabs.stories.tsx +68 -0
  200. package/template/apps/web/src/components/tabs/url-tabs.tsx +42 -0
  201. package/template/apps/web/src/components/textarea/textarea.stories.tsx +144 -0
  202. package/template/apps/web/src/components/textarea/textarea.tsx +115 -0
  203. package/template/apps/web/src/components/toast/toaster.tsx +38 -0
  204. package/template/apps/web/src/components/tooltip/tooltip.stories.tsx +237 -0
  205. package/template/apps/web/src/components/tooltip/tooltip.tsx +49 -0
  206. package/template/apps/web/src/config/csp.ts +59 -0
  207. package/template/apps/web/src/config/routes.ts +14 -0
  208. package/template/apps/web/src/config/site.ts +5 -0
  209. package/template/apps/web/src/env.ts +15 -0
  210. package/template/apps/web/src/features/auth/auth-card.tsx +20 -0
  211. package/template/apps/web/src/features/auth/auth-divider.tsx +14 -0
  212. package/template/apps/web/src/features/auth/google-auth-button.tsx +49 -0
  213. package/template/apps/web/src/features/auth/google-one-tap.tsx +23 -0
  214. package/template/apps/web/src/features/auth/lib/auth-error.test.ts +22 -0
  215. package/template/apps/web/src/features/auth/lib/auth-error.ts +16 -0
  216. package/template/apps/web/src/features/auth/lib/return-to.test.ts +41 -0
  217. package/template/apps/web/src/features/auth/lib/return-to.ts +19 -0
  218. package/template/apps/web/src/features/auth/lib/validator.test.ts +45 -0
  219. package/template/apps/web/src/features/auth/lib/validator.ts +49 -0
  220. package/template/apps/web/src/features/auth/passkey-settings.tsx +106 -0
  221. package/template/apps/web/src/features/auth/recovery-form.tsx +61 -0
  222. package/template/apps/web/src/features/auth/resend-verification-button.tsx +36 -0
  223. package/template/apps/web/src/features/auth/reset-password-form.tsx +62 -0
  224. package/template/apps/web/src/features/auth/sign-in-form.stories.tsx +22 -0
  225. package/template/apps/web/src/features/auth/sign-in-form.tsx +116 -0
  226. package/template/apps/web/src/features/auth/sign-out-button.tsx +26 -0
  227. package/template/apps/web/src/features/auth/sign-up-form.tsx +107 -0
  228. package/template/apps/web/src/features/shared/provider.tsx +16 -0
  229. package/template/apps/web/src/features/shared/site-header.tsx +48 -0
  230. package/template/apps/web/src/features/shared/theme-toggle.tsx +24 -0
  231. package/template/apps/web/src/i18n/navigation.ts +5 -0
  232. package/template/apps/web/src/i18n/request.ts +21 -0
  233. package/template/apps/web/src/i18n/routing.ts +6 -0
  234. package/template/apps/web/src/lib/auth-client.ts +19 -0
  235. package/template/apps/web/src/lib/auth-server.ts +46 -0
  236. package/template/apps/web/src/lib/auth.ts +8 -0
  237. package/template/apps/web/src/lib/codegen.ts +30 -0
  238. package/template/apps/web/src/lib/graphql-client.ts +29 -0
  239. package/template/apps/web/src/lib/graphql-server.ts +28 -0
  240. package/template/apps/web/src/lib/graphql-utils.test.ts +32 -0
  241. package/template/apps/web/src/lib/graphql-utils.ts +76 -0
  242. package/template/apps/web/src/lib/hooks/use-media-query.ts +21 -0
  243. package/template/apps/web/src/lib/metadata.ts +17 -0
  244. package/template/apps/web/src/lib/network-error.test.ts +30 -0
  245. package/template/apps/web/src/lib/network-error.ts +17 -0
  246. package/template/apps/web/src/lib/proxy-utils.ts +42 -0
  247. package/template/apps/web/src/lib/query-client.ts +29 -0
  248. package/template/apps/web/src/lib/utils.ts +5 -0
  249. package/template/apps/web/src/proxy.ts +57 -0
  250. package/template/apps/web/test-results/.last-run.json +4 -0
  251. package/template/apps/web/tests/auth.spec.ts +17 -0
  252. package/template/apps/web/tests/csp-theme-script.spec.ts +37 -0
  253. package/template/apps/web/tests/home.spec.ts +25 -0
  254. package/template/apps/web/tsconfig.build.json +4 -0
  255. package/template/apps/web/tsconfig.json +28 -0
  256. package/template/apps/web/vitest.config.ts +40 -0
  257. package/template/apps/web/vitest.shims.d.ts +1 -0
  258. package/template/apps/worker/.env.example +4 -0
  259. package/template/apps/worker/AGENTS.md +11 -0
  260. package/template/apps/worker/Dockerfile +35 -0
  261. package/template/apps/worker/knip.config.ts +3 -0
  262. package/template/apps/worker/kustomize/base/application.properties +4 -0
  263. package/template/apps/worker/kustomize/base/deployment.yaml +62 -0
  264. package/template/apps/worker/kustomize/base/kustomization.yaml +16 -0
  265. package/template/apps/worker/kustomize/overlays/prod/kustomization.yaml +12 -0
  266. package/template/apps/worker/kustomize/overlays/staging/application.properties +1 -0
  267. package/template/apps/worker/kustomize/overlays/staging/kustomization.yaml +18 -0
  268. package/template/apps/worker/oxfmt.config.ts +8 -0
  269. package/template/apps/worker/oxlint.config.ts +15 -0
  270. package/template/apps/worker/package.json +33 -0
  271. package/template/apps/worker/sonar-project.properties +8 -0
  272. package/template/apps/worker/src/env.ts +13 -0
  273. package/template/apps/worker/src/lib/graceful-shutdown.ts +27 -0
  274. package/template/apps/worker/src/lib/logger.ts +20 -0
  275. package/template/apps/worker/src/main.ts +17 -0
  276. package/template/apps/worker/src/processors/example-processor.ts +15 -0
  277. package/template/apps/worker/src/worker/redis.ts +19 -0
  278. package/template/apps/worker/src/worker/worker.ts +37 -0
  279. package/template/apps/worker/tests/example-processor.test.ts +13 -0
  280. package/template/apps/worker/tests/graceful-shutdown.test.ts +34 -0
  281. package/template/apps/worker/tsconfig.json +4 -0
  282. package/template/apps/worker/vitest.config.ts +13 -0
  283. package/template/bun.lock +3053 -0
  284. package/template/bunfig.toml +2 -0
  285. package/template/compose.yaml +53 -0
  286. package/template/docker-bake.hcl +86 -0
  287. package/template/package.json +47 -0
  288. package/template/packages/lint/knip.ts +40 -0
  289. package/template/packages/lint/oxfmt.ts +8 -0
  290. package/template/packages/lint/oxlint.ts +136 -0
  291. package/template/packages/lint/package.json +13 -0
  292. package/template/packages/queues/oxfmt.config.ts +8 -0
  293. package/template/packages/queues/oxlint.config.ts +7 -0
  294. package/template/packages/queues/package.json +15 -0
  295. package/template/packages/queues/src/example-job.ts +11 -0
  296. package/template/packages/queues/src/index.ts +3 -0
  297. package/template/packages/queues/src/queue-names.ts +2 -0
  298. package/template/packages/queues/src/redis-connection.ts +27 -0
  299. package/template/packages/queues/tsconfig.json +4 -0
  300. package/template/packages/tsconfig/base.json +23 -0
  301. package/template/packages/tsconfig/node.json +11 -0
  302. package/template/packages/tsconfig/package.json +4 -0
  303. package/template/turbo.json +50 -0
@@ -0,0 +1,209 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { Ban, Building2, Users } from "lucide-react";
3
+ import { expect, userEvent, within } from "storybook/test";
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
5
+
6
+ const meta: Meta<typeof Tabs> = {
7
+ component: Tabs,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ } satisfies Meta<typeof Tabs>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ play: async ({ canvasElement }) => {
19
+ const canvas = within(canvasElement);
20
+
21
+ await expect(canvas.getByText("12 total tasks")).toBeInTheDocument();
22
+
23
+ await userEvent.click(canvas.getByRole("tab", { name: "Active" }));
24
+ await expect(canvas.getByText("8 active tasks")).toBeInTheDocument();
25
+ },
26
+ render: () => (
27
+ <Tabs className="w-[400px]" defaultValue="all">
28
+ <TabsList className="grid w-full grid-cols-3">
29
+ <TabsTrigger value="all">All</TabsTrigger>
30
+ <TabsTrigger value="active">Active</TabsTrigger>
31
+ <TabsTrigger value="completed">Completed</TabsTrigger>
32
+ </TabsList>
33
+ <TabsContent className="mt-4 space-y-2" value="all">
34
+ <div className="border-border bg-card rounded-lg border p-4">
35
+ <p className="text-sm">12 total tasks</p>
36
+ </div>
37
+ </TabsContent>
38
+ <TabsContent className="mt-4 space-y-2" value="active">
39
+ <div className="border-border bg-card rounded-lg border p-4">
40
+ <p className="text-sm">8 active tasks</p>
41
+ </div>
42
+ </TabsContent>
43
+ <TabsContent className="mt-4 space-y-2" value="completed">
44
+ <div className="border-border bg-card rounded-lg border p-4">
45
+ <p className="text-sm">4 completed tasks</p>
46
+ </div>
47
+ </TabsContent>
48
+ </Tabs>
49
+ ),
50
+ };
51
+
52
+ export const WithIcons: Story = {
53
+ render: () => (
54
+ <Tabs className="w-[350px]" defaultValue="friends">
55
+ <TabsList className="grid w-full grid-cols-3">
56
+ <TabsTrigger value="friends">
57
+ <Users className="mr-2 h-4 w-4" />
58
+ Friends
59
+ </TabsTrigger>
60
+ <TabsTrigger value="institution">
61
+ <Building2 className="mr-2 h-4 w-4" />
62
+ Institution
63
+ </TabsTrigger>
64
+ <TabsTrigger value="blocked">
65
+ <Ban className="mr-2 h-4 w-4" />
66
+ Blocked
67
+ </TabsTrigger>
68
+ </TabsList>
69
+ <TabsContent className="mt-4" value="friends">
70
+ <div className="border-border bg-card rounded-lg border p-4">
71
+ <p className="text-sm font-medium">23 Friends</p>
72
+ <p className="text-muted-foreground mt-1 text-xs">People you follow</p>
73
+ </div>
74
+ </TabsContent>
75
+ <TabsContent className="mt-4" value="institution">
76
+ <div className="border-border bg-card rounded-lg border p-4">
77
+ <p className="text-sm font-medium">156 Members</p>
78
+ <p className="text-muted-foreground mt-1 text-xs">From your institution</p>
79
+ </div>
80
+ </TabsContent>
81
+ <TabsContent className="mt-4" value="blocked">
82
+ <div className="border-border bg-card rounded-lg border p-4">
83
+ <p className="text-sm font-medium">2 Blocked</p>
84
+ <p className="text-muted-foreground mt-1 text-xs">Users you've blocked</p>
85
+ </div>
86
+ </TabsContent>
87
+ </Tabs>
88
+ ),
89
+ };
90
+
91
+ export const IconOnly: Story = {
92
+ render: () => (
93
+ <Tabs className="w-[250px]" defaultValue="friends">
94
+ <TabsList className="grid h-8 w-full grid-cols-3 rounded-full p-0">
95
+ <TabsTrigger
96
+ aria-label="Friends"
97
+ className="data-[state=active]:bg-brand-500 data-[state=inactive]:hover:bg-brand-500/10 h-8 rounded-full data-[state=active]:text-white data-[state=active]:shadow-none"
98
+ value="friends"
99
+ >
100
+ <Users className="h-4 w-4" />
101
+ </TabsTrigger>
102
+ <TabsTrigger
103
+ aria-label="Institution"
104
+ className="data-[state=active]:bg-brand-500 data-[state=inactive]:hover:bg-brand-500/10 h-8 rounded-full data-[state=active]:text-white data-[state=active]:shadow-none"
105
+ value="institution"
106
+ >
107
+ <Building2 className="h-4 w-4" />
108
+ </TabsTrigger>
109
+ <TabsTrigger
110
+ aria-label="Blocked"
111
+ className="h-8 rounded-full data-[state=active]:bg-red-500 data-[state=active]:text-white data-[state=active]:shadow-none data-[state=inactive]:hover:bg-red-500/10"
112
+ value="blocked"
113
+ >
114
+ <Ban className="h-4 w-4" />
115
+ </TabsTrigger>
116
+ </TabsList>
117
+ <TabsContent value="friends">
118
+ <p className="text-muted-foreground text-sm">23 friends</p>
119
+ </TabsContent>
120
+ <TabsContent value="institution">
121
+ <p className="text-muted-foreground text-sm">156 members</p>
122
+ </TabsContent>
123
+ <TabsContent value="blocked">
124
+ <p className="text-muted-foreground text-sm">2 blocked</p>
125
+ </TabsContent>
126
+ </Tabs>
127
+ ),
128
+ };
129
+
130
+ export const CustomColors: Story = {
131
+ render: () => (
132
+ <Tabs className="w-[400px]" defaultValue="success">
133
+ <TabsList className="grid w-full grid-cols-3">
134
+ <TabsTrigger
135
+ className="data-[state=active]:bg-green-700 data-[state=active]:text-white"
136
+ value="success"
137
+ >
138
+ Success
139
+ </TabsTrigger>
140
+ <TabsTrigger
141
+ className="data-[state=active]:bg-amber-700 data-[state=active]:text-white"
142
+ value="warning"
143
+ >
144
+ Warning
145
+ </TabsTrigger>
146
+ <TabsTrigger
147
+ className="data-[state=active]:bg-red-700 data-[state=active]:text-white"
148
+ value="error"
149
+ >
150
+ Error
151
+ </TabsTrigger>
152
+ </TabsList>
153
+ <TabsContent className="mt-4" value="success">
154
+ <div className="rounded-lg border border-green-500/20 bg-green-500/10 p-4">
155
+ <p className="text-sm font-medium text-green-800 dark:text-green-400">
156
+ Operation successful
157
+ </p>
158
+ <p className="mt-1 text-xs text-green-800 dark:text-green-500">All systems operational</p>
159
+ </div>
160
+ </TabsContent>
161
+ <TabsContent className="mt-4" value="warning">
162
+ <div className="rounded-lg border border-amber-500/20 bg-amber-500/10 p-4">
163
+ <p className="text-sm font-medium text-amber-800 dark:text-amber-400">Warning detected</p>
164
+ <p className="mt-1 text-xs text-amber-800 dark:text-amber-500">
165
+ Some issues need attention
166
+ </p>
167
+ </div>
168
+ </TabsContent>
169
+ <TabsContent className="mt-4" value="error">
170
+ <div className="rounded-lg border border-red-500/20 bg-red-500/10 p-4">
171
+ <p className="text-sm font-medium text-red-800 dark:text-red-400">Error occurred</p>
172
+ <p className="mt-1 text-xs text-red-800 dark:text-red-500">Action required</p>
173
+ </div>
174
+ </TabsContent>
175
+ </Tabs>
176
+ ),
177
+ };
178
+
179
+ export const WithDisabledTab: Story = {
180
+ render: () => (
181
+ <Tabs className="w-[400px]" defaultValue="profile">
182
+ <TabsList className="grid w-full grid-cols-3">
183
+ <TabsTrigger value="profile">Profile</TabsTrigger>
184
+ <TabsTrigger value="settings">Settings</TabsTrigger>
185
+ <TabsTrigger disabled value="billing">
186
+ Billing
187
+ </TabsTrigger>
188
+ </TabsList>
189
+ <TabsContent className="mt-4" value="profile">
190
+ <div className="border-border bg-card rounded-lg border p-4">
191
+ <p className="text-sm font-medium">Your Profile</p>
192
+ <p className="text-muted-foreground mt-1 text-xs">Manage your personal information</p>
193
+ </div>
194
+ </TabsContent>
195
+ <TabsContent className="mt-4" value="settings">
196
+ <div className="border-border bg-card rounded-lg border p-4">
197
+ <p className="text-sm font-medium">Settings</p>
198
+ <p className="text-muted-foreground mt-1 text-xs">Configure your preferences</p>
199
+ </div>
200
+ </TabsContent>
201
+ <TabsContent className="mt-4" value="billing">
202
+ <div className="border-border bg-card rounded-lg border p-4">
203
+ <p className="text-sm font-medium">Billing</p>
204
+ <p className="text-muted-foreground mt-1 text-xs">Upgrade to access billing</p>
205
+ </div>
206
+ </TabsContent>
207
+ </Tabs>
208
+ ),
209
+ };
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import * as TabsPrimitive from "@radix-ui/react-tabs";
4
+ import * as React from "react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const Tabs = TabsPrimitive.Root;
8
+
9
+ function TabsList({
10
+ className,
11
+ ref,
12
+ ...props
13
+ }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
14
+ ref?: React.RefObject<null | React.ComponentRef<typeof TabsPrimitive.List>>;
15
+ }) {
16
+ return (
17
+ <TabsPrimitive.List
18
+ className={cn(
19
+ "flex items-center justify-center rounded-full bg-muted/70 p-1 text-muted-foreground",
20
+ className,
21
+ )}
22
+ ref={ref}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+ TabsList.displayName = TabsPrimitive.List.displayName;
28
+
29
+ function TabsTrigger({
30
+ className,
31
+ ref,
32
+ ...props
33
+ }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {
34
+ ref?: React.RefObject<null | React.ComponentRef<typeof TabsPrimitive.Trigger>>;
35
+ }) {
36
+ return (
37
+ <TabsPrimitive.Trigger
38
+ className={cn(
39
+ "inline-flex cursor-pointer items-center justify-center whitespace-nowrap rounded-full px-3.5 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-primary data-[state=active]:text-primary-foreground data-[state=active]:shadow-sm data-[state=inactive]:hover:bg-card",
40
+ className,
41
+ )}
42
+ ref={ref}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
48
+
49
+ function TabsContent({
50
+ className,
51
+ ref,
52
+ ...props
53
+ }: React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> & {
54
+ ref?: React.RefObject<null | React.ComponentRef<typeof TabsPrimitive.Content>>;
55
+ }) {
56
+ return (
57
+ <TabsPrimitive.Content
58
+ className={cn(
59
+ "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
60
+ className,
61
+ )}
62
+ ref={ref}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
68
+
69
+ export { Tabs, TabsContent, TabsList, TabsTrigger };
@@ -0,0 +1,68 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { expect, userEvent, within } from "storybook/test";
3
+ import { TabsContent, TabsList, TabsTrigger } from "./tabs";
4
+ import { UrlTabs } from "./url-tabs";
5
+
6
+ const TABS = ["overview", "activity", "settings"] as const;
7
+
8
+ function UrlTabsExample() {
9
+ return (
10
+ <UrlTabs className="w-96" defaultValue="overview" tabs={TABS}>
11
+ <TabsList className="w-full">
12
+ {TABS.map((tab) => (
13
+ <TabsTrigger className="flex-1 capitalize" key={tab} value={tab}>
14
+ {tab}
15
+ </TabsTrigger>
16
+ ))}
17
+ </TabsList>
18
+ {TABS.map((tab) => (
19
+ <TabsContent className="text-muted-foreground p-4 text-sm capitalize" key={tab} value={tab}>
20
+ {tab} panel
21
+ </TabsContent>
22
+ ))}
23
+ </UrlTabs>
24
+ );
25
+ }
26
+
27
+ /**
28
+ * `Tabs` that mirror the active tab into `?tab=` so it survives reload and is
29
+ * shareable. The default tab keeps the URL clean.
30
+ */
31
+ const meta = {
32
+ component: UrlTabsExample,
33
+ parameters: {
34
+ layout: "centered",
35
+ nextjs: { navigation: { pathname: "/institutions/kit", query: {} } },
36
+ },
37
+ tags: ["autodocs"],
38
+ } satisfies Meta<typeof UrlTabsExample>;
39
+
40
+ export default meta;
41
+
42
+ type Story = StoryObj<typeof meta>;
43
+
44
+ export const Default: Story = {
45
+ play: async ({ canvasElement }) => {
46
+ const canvas = within(canvasElement);
47
+
48
+ await expect(canvas.getAllByRole("tab")).toHaveLength(3);
49
+ await expect(canvas.getByRole("tab", { name: "overview" })).toHaveAttribute(
50
+ "data-state",
51
+ "active",
52
+ );
53
+
54
+ await userEvent.click(canvas.getByRole("tab", { name: "activity" }));
55
+ },
56
+ };
57
+
58
+ export const DeepLinkedTab: Story = {
59
+ parameters: {
60
+ nextjs: { navigation: { pathname: "/institutions/kit", query: { tab: "settings" } } },
61
+ },
62
+ play: async ({ canvasElement }) => {
63
+ await expect(within(canvasElement).getByRole("tab", { name: "settings" })).toHaveAttribute(
64
+ "data-state",
65
+ "active",
66
+ );
67
+ },
68
+ };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { usePathname, useRouter, useSearchParams } from "next/navigation";
4
+ import type { ComponentPropsWithoutRef } from "react";
5
+ import { Tabs } from "./tabs";
6
+
7
+ type Props = Omit<ComponentPropsWithoutRef<typeof Tabs>, "onValueChange" | "value"> & {
8
+ defaultValue: string;
9
+ paramName?: string;
10
+ tabs: readonly string[];
11
+ };
12
+
13
+ /**
14
+ * Drop-in replacement for `Tabs` that mirrors the active tab into the URL
15
+ * (`?tab=...`) so switching tabs is shareable/back-button-able and survives a
16
+ * reload, while keeping the default tab's URL clean.
17
+ */
18
+ export function UrlTabs({ defaultValue, paramName = "tab", tabs, ...props }: Props) {
19
+ const router = useRouter();
20
+ const pathname = usePathname();
21
+ const searchParams = useSearchParams();
22
+
23
+ const tabParam = searchParams.get(paramName);
24
+ const value = tabParam && tabs.includes(tabParam) ? tabParam : defaultValue;
25
+
26
+ return (
27
+ <Tabs
28
+ {...props}
29
+ onValueChange={(next) => {
30
+ const params = new URLSearchParams(searchParams);
31
+ if (next === defaultValue) {
32
+ params.delete(paramName);
33
+ } else {
34
+ params.set(paramName, next);
35
+ }
36
+ const query = params.toString();
37
+ router.replace(`${pathname}${query ? `?${query}` : ""}`, { scroll: false });
38
+ }}
39
+ value={value}
40
+ />
41
+ );
42
+ }
@@ -0,0 +1,144 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { useState } from "react";
3
+ import { expect, userEvent, within } from "storybook/test";
4
+ import { Textarea } from "./textarea";
5
+
6
+ const meta: Meta<typeof Textarea> = {
7
+ component: Textarea,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ } satisfies Meta<typeof Textarea>;
13
+
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ play: async ({ canvasElement }) => {
19
+ const field = within(canvasElement).getByPlaceholderText("Type your message here...");
20
+
21
+ await userEvent.type(field, "Hello there");
22
+ await expect(field).toHaveValue("Hello there");
23
+ },
24
+ render: () => <Textarea className="w-[400px]" placeholder="Type your message here..." />,
25
+ };
26
+
27
+ export const WithLabel: Story = {
28
+ render: () => (
29
+ <Textarea className="w-[400px]" label="Message" placeholder="Type your message here..." />
30
+ ),
31
+ };
32
+
33
+ export const WithHint: Story = {
34
+ render: () => (
35
+ <Textarea
36
+ className="w-[400px]"
37
+ hint="This will be displayed on your public profile."
38
+ label="Bio"
39
+ placeholder="Tell us about yourself..."
40
+ />
41
+ ),
42
+ };
43
+
44
+ export const WithError: Story = {
45
+ render: () => (
46
+ <Textarea
47
+ className="w-[400px]"
48
+ error="Description must be at least 10 characters long."
49
+ label="Description"
50
+ placeholder="Enter a description..."
51
+ />
52
+ ),
53
+ };
54
+
55
+ export const Success: Story = {
56
+ render: () => (
57
+ <Textarea
58
+ className="w-[400px]"
59
+ defaultValue="This product exceeded my expectations! The quality is outstanding."
60
+ hint="Your review looks great!"
61
+ isSuccess
62
+ label="Review"
63
+ placeholder="Write your review..."
64
+ />
65
+ ),
66
+ };
67
+
68
+ export const Disabled: Story = {
69
+ render: () => (
70
+ <Textarea
71
+ className="w-[400px]"
72
+ defaultValue="This textarea is disabled and cannot be edited."
73
+ disabled
74
+ label="Readonly Message"
75
+ placeholder="Type your message here..."
76
+ />
77
+ ),
78
+ };
79
+
80
+ export const SmallSize: Story = {
81
+ render: () => (
82
+ <Textarea
83
+ className="w-[400px]"
84
+ label="Quick Note"
85
+ placeholder="Add a quick note..."
86
+ size="sm"
87
+ />
88
+ ),
89
+ };
90
+
91
+ export const WithRows: Story = {
92
+ render: () => (
93
+ <Textarea
94
+ className="w-[500px]"
95
+ hint="Minimum 500 words required."
96
+ label="Essay"
97
+ placeholder="Write your essay here..."
98
+ rows={10}
99
+ />
100
+ ),
101
+ };
102
+
103
+ export const WithCharacterCount: Story = {
104
+ render: () => {
105
+ const [value, setValue] = useState("");
106
+ const maxLength = 200;
107
+
108
+ return (
109
+ <Textarea
110
+ className="w-[400px]"
111
+ hint={`${value.length}/${maxLength} characters`}
112
+ label="Tweet"
113
+ maxLength={maxLength}
114
+ onChange={(e) => setValue(e.target.value)}
115
+ placeholder="What's happening?"
116
+ value={value}
117
+ />
118
+ );
119
+ },
120
+ };
121
+
122
+ export const InForm: Story = {
123
+ render: () => (
124
+ <div className="w-[500px] space-y-6">
125
+ <div className="space-y-4 rounded-2xl border border-white/20 bg-white/10 p-6 backdrop-blur-sm">
126
+ <h3 className="text-lg font-semibold">Contact Form</h3>
127
+
128
+ <Textarea
129
+ hint="Please provide as much detail as possible."
130
+ label="Message"
131
+ placeholder="How can we help you?"
132
+ rows={4}
133
+ />
134
+
135
+ <Textarea
136
+ label="Additional Notes"
137
+ placeholder="Any additional information..."
138
+ rows={3}
139
+ size="sm"
140
+ />
141
+ </div>
142
+ </div>
143
+ ),
144
+ };
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import type { TextareaHTMLAttributes } from "react";
4
+ import { useId } from "react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ type TextareaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "ref"> & {
8
+ containerClassName?: string;
9
+ error?: string;
10
+ frameClassName?: string;
11
+ hint?: string;
12
+ isSuccess?: boolean;
13
+ label?: string;
14
+ ref?: React.Ref<HTMLTextAreaElement>;
15
+ size?: "md" | "sm";
16
+ };
17
+
18
+ const FRAME_BASE =
19
+ "flex w-full rounded-xl border bg-card transition-[border-color,box-shadow] duration-150";
20
+ const FRAME_SIZE = { md: "min-h-[120px] px-3.5 py-2.5", sm: "min-h-[80px] px-3 py-2" } as const;
21
+ const TEXTAREA_BASE =
22
+ "w-full min-w-0 resize-none bg-transparent text-foreground outline-none placeholder:text-muted-foreground/60 focus-visible:outline-none";
23
+ const TEXTAREA_SIZE = { md: "text-base", sm: "text-sm" } as const;
24
+
25
+ const FRAME_STATE = {
26
+ default:
27
+ "border-input shadow-xs focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/25",
28
+ disabled: "cursor-not-allowed border-input bg-muted/50 shadow-none dark:bg-muted/30",
29
+ error:
30
+ "border-destructive/60 shadow-xs focus-within:border-destructive focus-within:ring-2 focus-within:ring-destructive/20",
31
+ success:
32
+ "border-emerald-500/50 shadow-xs focus-within:border-emerald-500 focus-within:ring-2 focus-within:ring-emerald-500/20",
33
+ } as const;
34
+
35
+ function Textarea(props: TextareaProps) {
36
+ const {
37
+ className,
38
+ containerClassName,
39
+ disabled,
40
+ error,
41
+ frameClassName,
42
+ hint,
43
+ id,
44
+ isSuccess,
45
+ label,
46
+ ref,
47
+ size = "md",
48
+ ...textareaProps
49
+ } = props;
50
+
51
+ const generatedId = useId();
52
+
53
+ const textareaId = id ?? `${generatedId}-textarea`;
54
+ const hasError = Boolean(error);
55
+ const hasSuccess = isSuccess && !hasError;
56
+
57
+ const describedBy =
58
+ [
59
+ hasError && `${textareaId}-error`,
60
+ hasSuccess && `${textareaId}-success`,
61
+ hint && !hasError && `${textareaId}-hint`,
62
+ ]
63
+ .filter(Boolean)
64
+ .join(" ") || undefined;
65
+
66
+ const state = disabled ? "disabled" : hasError ? "error" : hasSuccess ? "success" : "default";
67
+
68
+ return (
69
+ <fieldset
70
+ className={cn(
71
+ "text-foreground/90 flex flex-col gap-y-1.5 text-sm",
72
+ disabled && "opacity-70",
73
+ containerClassName,
74
+ )}
75
+ >
76
+ {label && (
77
+ <label className="ml-0.5 text-sm font-medium" htmlFor={textareaId}>
78
+ {label}
79
+ </label>
80
+ )}
81
+
82
+ <div className={cn(FRAME_BASE, FRAME_SIZE[size], FRAME_STATE[state], frameClassName)}>
83
+ <textarea
84
+ {...textareaProps}
85
+ aria-describedby={describedBy}
86
+ aria-invalid={hasError}
87
+ className={cn(
88
+ TEXTAREA_BASE,
89
+ TEXTAREA_SIZE[size],
90
+ disabled && "cursor-not-allowed",
91
+ className,
92
+ )}
93
+ disabled={disabled}
94
+ id={textareaId}
95
+ ref={ref}
96
+ />
97
+ </div>
98
+
99
+ {hasError && error && (
100
+ <p className="text-destructive ml-0.5 text-xs font-medium" id={`${textareaId}-error`}>
101
+ {error}
102
+ </p>
103
+ )}
104
+ {!hasError && hint && (
105
+ <p className="text-muted-foreground ml-0.5 text-xs" id={`${textareaId}-hint`}>
106
+ {hint}
107
+ </p>
108
+ )}
109
+ </fieldset>
110
+ );
111
+ }
112
+
113
+ Textarea.displayName = "Textarea";
114
+
115
+ export { Textarea };
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import { Toaster as Sonner } from "sonner";
4
+
5
+ export function Toaster() {
6
+ return (
7
+ <Sonner
8
+ closeButton
9
+ expand={false}
10
+ gap={10}
11
+ position="bottom-right"
12
+ toastOptions={{
13
+ classNames: {
14
+ actionButton:
15
+ "!rounded-md !bg-foreground !px-2.5 !py-1 !text-[11px] !font-medium !text-background transition-colors hover:!bg-foreground/90",
16
+ cancelButton:
17
+ "!rounded-md !bg-muted !px-2.5 !py-1 !text-[11px] !font-medium !text-muted-foreground transition-colors hover:!bg-muted/80",
18
+ closeButton:
19
+ "!absolute !left-auto !right-2.5 !top-2.5 !rounded-md !border-0 !bg-transparent !p-0.5 !text-foreground/40 !opacity-0 !shadow-none transition-all hover:!text-foreground focus:!opacity-100 focus:!outline-none focus-visible:!ring-2 focus-visible:!ring-ring group-hover:!opacity-100 [&_svg]:h-3.5 [&_svg]:w-3.5",
20
+ content: "flex flex-col gap-0.5",
21
+ description: "text-[12px] leading-snug text-muted-foreground",
22
+ error:
23
+ "!border-l-red-500 [&_svg]:text-red-500 dark:!border-l-red-400 dark:[&_svg]:text-red-400",
24
+ icon: "flex h-[18px] w-[18px] shrink-0 items-center justify-center [&_svg]:h-[18px] [&_svg]:w-[18px]",
25
+ info: "!border-l-blue-500 [&_svg]:text-blue-500 dark:!border-l-blue-400 dark:[&_svg]:text-blue-400",
26
+ success:
27
+ "!border-l-emerald-500 [&_svg]:text-emerald-500 dark:!border-l-emerald-400 dark:[&_svg]:text-emerald-400",
28
+ title: "text-[13px] font-medium leading-tight text-foreground",
29
+ toast:
30
+ "group relative flex w-full min-w-[300px] max-w-[380px] items-center gap-3 overflow-hidden rounded-2xl border border-border/70 border-l-[3px] bg-popover py-3 pl-4 pr-9 shadow-[0_12px_32px_-12px_rgba(0,0,0,0.22)] transition-all",
31
+ warning:
32
+ "!border-l-amber-500 [&_svg]:text-amber-500 dark:!border-l-amber-400 dark:[&_svg]:text-amber-400",
33
+ },
34
+ unstyled: true,
35
+ }}
36
+ />
37
+ );
38
+ }