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,33 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { expect, fn, userEvent, within } from "storybook/test";
3
+ import { Button } from "./button";
4
+
5
+ const meta = {
6
+ args: { children: "Save changes", onClick: fn() },
7
+ component: Button,
8
+ parameters: { layout: "centered" },
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof Button>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Primary: Story = {
16
+ play: async ({ args, canvasElement }) => {
17
+ await userEvent.click(within(canvasElement).getByRole("button", { name: "Save changes" }));
18
+ await expect(args.onClick).toHaveBeenCalledOnce();
19
+ },
20
+ };
21
+
22
+ export const Secondary: Story = { args: { variant: "secondary" } };
23
+
24
+ export const Outline: Story = { args: { variant: "outline" } };
25
+
26
+ export const Destructive: Story = { args: { children: "Delete", variant: "destructive" } };
27
+
28
+ export const Loading: Story = {
29
+ args: { isLoading: true },
30
+ play: async ({ canvasElement }) => {
31
+ await expect(within(canvasElement).getByRole("button")).toBeDisabled();
32
+ },
33
+ };
@@ -0,0 +1,32 @@
1
+ import type { VariantProps } from "class-variance-authority";
2
+ import type { ComponentPropsWithRef } from "react";
3
+ import { Spinner } from "@/components/spinner/spinner";
4
+ import { cn } from "@/lib/utils";
5
+ import { buttonVariants } from "./button-variants";
6
+
7
+ export type ButtonProps = ComponentPropsWithRef<"button"> &
8
+ VariantProps<typeof buttonVariants> & { isLoading?: boolean };
9
+
10
+ export function Button({
11
+ children,
12
+ className,
13
+ disabled,
14
+ isLoading = false,
15
+ size,
16
+ type = "button",
17
+ variant,
18
+ ...rest
19
+ }: ButtonProps) {
20
+ return (
21
+ <button
22
+ aria-busy={isLoading || undefined}
23
+ className={cn(buttonVariants({ size, variant }), className)}
24
+ disabled={disabled || isLoading}
25
+ type={type}
26
+ {...rest}
27
+ >
28
+ {isLoading ? <Spinner aria-hidden className="shrink-0" size={18} /> : null}
29
+ {children}
30
+ </button>
31
+ );
32
+ }
@@ -0,0 +1,312 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import Link from "next/link";
3
+ import { useState } from "react";
4
+ import { expect, fn, userEvent, within } from "storybook/test";
5
+ import { Checkbox } from "./checkbox";
6
+
7
+ const meta: Meta<typeof Checkbox> = {
8
+ args: {
9
+ "aria-label": "Accept terms and conditions",
10
+ },
11
+ component: Checkbox,
12
+ parameters: {
13
+ layout: "centered",
14
+ },
15
+ tags: ["autodocs"],
16
+ } satisfies Meta<typeof Checkbox>;
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof meta>;
20
+
21
+ export const Default: Story = {
22
+ args: {
23
+ onCheckedChange: fn(),
24
+ },
25
+ play: async ({ args, canvasElement }) => {
26
+ const canvas = within(canvasElement);
27
+ const checkbox = canvas.getByRole("checkbox");
28
+
29
+ await expect(checkbox).toBeInTheDocument();
30
+ await expect(checkbox).not.toBeChecked();
31
+ await expect(checkbox).not.toBeDisabled();
32
+
33
+ await userEvent.click(checkbox);
34
+
35
+ await expect(checkbox).toBeChecked();
36
+ await expect(args.onCheckedChange).toHaveBeenCalledWith(true);
37
+
38
+ await userEvent.click(checkbox);
39
+
40
+ await expect(checkbox).not.toBeChecked();
41
+ await expect(args.onCheckedChange).toHaveBeenCalledWith(false);
42
+ },
43
+ };
44
+
45
+ export const Checked: Story = {
46
+ args: {
47
+ defaultChecked: true,
48
+ },
49
+ play: async ({ canvasElement }) => {
50
+ const canvas = within(canvasElement);
51
+ const checkbox = canvas.getByRole("checkbox");
52
+
53
+ await expect(checkbox).toBeInTheDocument();
54
+ await expect(checkbox).toBeChecked();
55
+ },
56
+ };
57
+
58
+ export const Disabled: Story = {
59
+ args: {
60
+ disabled: true,
61
+ onCheckedChange: fn(),
62
+ },
63
+ play: async ({ args, canvasElement }) => {
64
+ const canvas = within(canvasElement);
65
+ const checkbox = canvas.getByRole("checkbox");
66
+
67
+ await expect(checkbox).toBeInTheDocument();
68
+ await expect(checkbox).toBeDisabled();
69
+ await expect(checkbox).not.toBeChecked();
70
+
71
+ await userEvent.click(checkbox);
72
+
73
+ await expect(checkbox).not.toBeChecked();
74
+ await expect(args.onCheckedChange).not.toHaveBeenCalled();
75
+ },
76
+ };
77
+
78
+ export const DisabledChecked: Story = {
79
+ args: {
80
+ defaultChecked: true,
81
+ disabled: true,
82
+ },
83
+ play: async ({ canvasElement }) => {
84
+ const canvas = within(canvasElement);
85
+ const checkbox = canvas.getByRole("checkbox");
86
+
87
+ await expect(checkbox).toBeInTheDocument();
88
+ await expect(checkbox).toBeDisabled();
89
+ await expect(checkbox).toBeChecked();
90
+
91
+ await userEvent.click(checkbox);
92
+
93
+ await expect(checkbox).toBeChecked();
94
+ },
95
+ };
96
+
97
+ export const WithLabel: Story = {
98
+ play: async ({ canvasElement }) => {
99
+ const canvas = within(canvasElement);
100
+ const checkbox = canvas.getByRole("checkbox");
101
+ const label = canvas.getByText("Accept terms and conditions");
102
+
103
+ await expect(checkbox).toBeInTheDocument();
104
+ await expect(label).toBeInTheDocument();
105
+ await expect(checkbox).not.toBeChecked();
106
+
107
+ await userEvent.click(label);
108
+
109
+ await expect(checkbox).toBeChecked();
110
+
111
+ await userEvent.click(checkbox);
112
+
113
+ await expect(checkbox).not.toBeChecked();
114
+ },
115
+ render: () => {
116
+ const [checked, setChecked] = useState(false);
117
+
118
+ return (
119
+ <div className="flex items-center space-x-2">
120
+ <Checkbox
121
+ checked={checked}
122
+ id="terms"
123
+ onCheckedChange={(value) => setChecked(value as boolean)}
124
+ />
125
+ <label
126
+ className="cursor-pointer text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
127
+ htmlFor="terms"
128
+ >
129
+ Accept terms and conditions
130
+ </label>
131
+ </div>
132
+ );
133
+ },
134
+ };
135
+
136
+ export const WithLongLabel: Story = {
137
+ play: async ({ canvasElement }) => {
138
+ const canvas = within(canvasElement);
139
+ const checkbox = canvas.getByRole("checkbox");
140
+ const label = canvas.getByText(/I agree to the terms/i);
141
+
142
+ await expect(checkbox).toBeInTheDocument();
143
+ await expect(label).toBeInTheDocument();
144
+ await expect(checkbox).not.toBeChecked();
145
+
146
+ await userEvent.click(label);
147
+
148
+ await expect(checkbox).toBeChecked();
149
+ },
150
+ render: () => {
151
+ const [checked, setChecked] = useState(false);
152
+
153
+ return (
154
+ <div className="flex max-w-md items-start space-x-2">
155
+ <Checkbox
156
+ checked={checked}
157
+ className="mt-1"
158
+ id="terms-long"
159
+ onCheckedChange={(value) => setChecked(value as boolean)}
160
+ />
161
+ <label
162
+ className="cursor-pointer text-sm leading-relaxed font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
163
+ htmlFor="terms-long"
164
+ >
165
+ I agree to the terms and conditions, privacy policy, and understand that my data will be
166
+ processed according to the GDPR regulations.
167
+ </label>
168
+ </div>
169
+ );
170
+ },
171
+ };
172
+
173
+ export const FormExample: Story = {
174
+ play: async ({ canvasElement }) => {
175
+ const canvas = within(canvasElement);
176
+ const checkbox = canvas.getByRole("checkbox");
177
+ const submitButton = canvas.getByRole("button", { name: /sign up/i });
178
+
179
+ await expect(checkbox).toBeInTheDocument();
180
+ await expect(checkbox).not.toBeChecked();
181
+ await expect(submitButton).toBeDisabled();
182
+
183
+ await userEvent.click(checkbox);
184
+
185
+ await expect(checkbox).toBeChecked();
186
+ await expect(submitButton).toBeEnabled();
187
+
188
+ await userEvent.click(checkbox);
189
+
190
+ await expect(checkbox).not.toBeChecked();
191
+ await expect(submitButton).toBeDisabled();
192
+ },
193
+ render: () => {
194
+ const [checked, setChecked] = useState(false);
195
+
196
+ return (
197
+ <form className="max-w-md space-y-4 rounded-lg border p-6">
198
+ <h3 className="text-lg font-semibold">Sign Up</h3>
199
+
200
+ <div className="space-y-2">
201
+ <label className="text-sm font-medium" htmlFor="signup-email">
202
+ Email
203
+ </label>
204
+ <input
205
+ aria-label="Email"
206
+ className="w-full rounded-md border px-3 py-2"
207
+ id="signup-email"
208
+ placeholder="email@example.com"
209
+ type="email"
210
+ />
211
+ </div>
212
+
213
+ <div className="space-y-2">
214
+ <label className="text-sm font-medium" htmlFor="signup-password">
215
+ Password
216
+ </label>
217
+ <input
218
+ aria-label="Password"
219
+ className="w-full rounded-md border px-3 py-2"
220
+ id="signup-password"
221
+ placeholder="********"
222
+ type="password"
223
+ />
224
+ </div>
225
+
226
+ <div className="flex items-start space-x-2">
227
+ <Checkbox
228
+ checked={checked}
229
+ className="mt-1"
230
+ id="signup-terms"
231
+ onCheckedChange={(value) => setChecked(value as boolean)}
232
+ />
233
+ <label className="cursor-pointer text-sm leading-relaxed" htmlFor="signup-terms">
234
+ By signing up, you agree to our{" "}
235
+ <Link className="text-brand-600 underline" href="#">
236
+ Terms of Service
237
+ </Link>{" "}
238
+ and{" "}
239
+ <Link className="text-brand-600 underline" href="#">
240
+ Privacy Policy
241
+ </Link>
242
+ </label>
243
+ </div>
244
+
245
+ <button
246
+ className="bg-brand-600 hover:bg-brand-700 w-full rounded-md px-4 py-2 text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
247
+ disabled={!checked}
248
+ type="submit"
249
+ >
250
+ Sign Up
251
+ </button>
252
+ </form>
253
+ );
254
+ },
255
+ };
256
+
257
+ export const MultipleCheckboxes: Story = {
258
+ play: async ({ canvasElement }) => {
259
+ const canvas = within(canvasElement);
260
+ const checkboxes = canvas.getAllByRole("checkbox");
261
+
262
+ await expect(checkboxes).toHaveLength(4);
263
+
264
+ await expect(checkboxes[0]).toBeChecked();
265
+ await expect(checkboxes[1]).not.toBeChecked();
266
+ await expect(checkboxes[2]).toBeChecked();
267
+ await expect(checkboxes[3]).not.toBeChecked();
268
+
269
+ await userEvent.click(checkboxes[1]);
270
+
271
+ await expect(checkboxes[1]).toBeChecked();
272
+
273
+ await userEvent.click(checkboxes[0]);
274
+
275
+ await expect(checkboxes[0]).not.toBeChecked();
276
+
277
+ const smsLabel = canvas.getByText("Receive SMS notifications");
278
+ await userEvent.click(smsLabel);
279
+
280
+ await expect(checkboxes[1]).not.toBeChecked();
281
+ },
282
+ render: () => {
283
+ const [items, setItems] = useState([
284
+ { checked: true, id: "1", label: "Receive email notifications" },
285
+ { checked: false, id: "2", label: "Receive SMS notifications" },
286
+ { checked: true, id: "3", label: "Receive push notifications" },
287
+ { checked: false, id: "4", label: "Marketing emails" },
288
+ ]);
289
+
290
+ const handleToggle = (id: string) => {
291
+ setItems(items.map((item) => (item.id === id ? { ...item, checked: !item.checked } : item)));
292
+ };
293
+
294
+ return (
295
+ <div className="max-w-md space-y-3 rounded-lg border p-6">
296
+ <h3 className="mb-4 text-lg font-semibold">Notification Preferences</h3>
297
+ {items.map((item) => (
298
+ <div className="flex items-center space-x-2" key={item.id}>
299
+ <Checkbox
300
+ checked={item.checked}
301
+ id={item.id}
302
+ onCheckedChange={() => handleToggle(item.id)}
303
+ />
304
+ <label className="cursor-pointer text-sm font-medium" htmlFor={item.id}>
305
+ {item.label}
306
+ </label>
307
+ </div>
308
+ ))}
309
+ </div>
310
+ );
311
+ },
312
+ };
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
4
+ import { Check } from "lucide-react";
5
+ import type { ComponentPropsWithoutRef, ComponentRef, RefObject } from "react";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function Checkbox({
9
+ className,
10
+ ref,
11
+ ...props
12
+ }: ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
13
+ ref?: RefObject<ComponentRef<typeof CheckboxPrimitive.Root> | null>;
14
+ }) {
15
+ return (
16
+ <CheckboxPrimitive.Root
17
+ className={cn(
18
+ "peer border-input bg-background flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-[6px] border shadow-xs outline-none",
19
+ "transition-[border-color,background-color,box-shadow] duration-150",
20
+ "dark:bg-input/20",
21
+ "hover:border-ring/60",
22
+ "focus-visible:border-ring focus-visible:ring-ring/25 focus-visible:ring-2",
23
+ "data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
24
+ "disabled:bg-muted/50 dark:disabled:bg-muted/30 disabled:cursor-not-allowed disabled:opacity-70 disabled:shadow-none",
25
+ className,
26
+ )}
27
+ ref={ref}
28
+ {...props}
29
+ >
30
+ <CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
31
+ <Check className="size-3.5" strokeWidth={3} />
32
+ </CheckboxPrimitive.Indicator>
33
+ </CheckboxPrimitive.Root>
34
+ );
35
+ }
36
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
37
+
38
+ export { Checkbox };
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { expect, within } from "storybook/test";
3
+ import { DangerPanel } from "./danger-panel";
4
+
5
+ const meta = {
6
+ args: {
7
+ message:
8
+ "This permanently removes the item and everything attached to it. This cannot be undone.",
9
+ title: "This action is irreversible",
10
+ },
11
+ component: DangerPanel,
12
+ decorators: [
13
+ (Story) => (
14
+ <div className="w-[26rem]">
15
+ <Story />
16
+ </div>
17
+ ),
18
+ ],
19
+ parameters: { layout: "centered" },
20
+ tags: ["autodocs"],
21
+ } satisfies Meta<typeof DangerPanel>;
22
+
23
+ export default meta;
24
+
25
+ type Story = StoryObj<typeof meta>;
26
+
27
+ export const Default: Story = {
28
+ play: async ({ canvasElement }) => {
29
+ const alert = within(canvasElement).getByRole("alert");
30
+
31
+ await expect(alert).toHaveTextContent("This action is irreversible");
32
+ },
33
+ };
34
+
35
+ export const DeleteAccount: Story = {
36
+ args: {
37
+ message:
38
+ "Your profile, uploads, comments and karma will be deleted. Study materials you chose to keep stay available anonymously.",
39
+ title: "Deleting your account is permanent",
40
+ },
41
+ };
@@ -0,0 +1,32 @@
1
+ import { AlertTriangle } from "lucide-react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ type DangerPanelProps = {
5
+ className?: string;
6
+ message: string;
7
+ title: string;
8
+ };
9
+
10
+ /**
11
+ * Destructive warning callout inside a confirmation dialog. Shared by
12
+ * `DeleteConfirmDialog` and `DeleteAccountDialog`, which each carried their own
13
+ * copy of this markup.
14
+ */
15
+ export function DangerPanel({ className, message, title }: DangerPanelProps) {
16
+ return (
17
+ <div
18
+ className={cn("border-destructive/20 bg-destructive/5 rounded-lg border p-4", className)}
19
+ role="alert"
20
+ >
21
+ <div className="flex gap-3">
22
+ <div className="bg-destructive/10 ring-destructive/20 flex h-10 w-10 shrink-0 items-center justify-center rounded-lg ring-1">
23
+ <AlertTriangle aria-hidden className="text-destructive h-5 w-5" />
24
+ </div>
25
+ <div className="flex-1 space-y-1">
26
+ <p className="text-foreground text-sm font-semibold">{title}</p>
27
+ <p className="text-muted-foreground text-xs leading-relaxed">{message}</p>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ );
32
+ }