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,307 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { AlertCircle, FileText, Mail, Share2, Trash2, Upload } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { expect, fn, userEvent, within } from "storybook/test";
5
+ import { Button } from "../button/button";
6
+ import { ResponsiveDialog } from "./responsive-dialog";
7
+
8
+ const meta = {
9
+ argTypes: {
10
+ open: {
11
+ control: "boolean",
12
+ },
13
+ },
14
+ component: ResponsiveDialog,
15
+ parameters: {
16
+ layout: "centered",
17
+ },
18
+ tags: ["autodocs"],
19
+ } satisfies Meta<typeof ResponsiveDialog>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ open: false,
27
+ setOpen: () => {},
28
+ title: "",
29
+ },
30
+ play: async ({ canvasElement }) => {
31
+ const canvas = within(canvasElement);
32
+ const trigger = canvas.getByRole("button", { name: /open dialog/i });
33
+
34
+ await expect(trigger).toBeInTheDocument();
35
+
36
+ await userEvent.click(trigger);
37
+ },
38
+ render: () => {
39
+ const [open, setOpen] = useState(false);
40
+
41
+ return (
42
+ <>
43
+ <Button onClick={() => setOpen(true)}>Open Dialog</Button>
44
+ <ResponsiveDialog
45
+ description="This dialog adapts to screen size - it's a dialog on desktop and a drawer on mobile."
46
+ open={open}
47
+ setOpen={setOpen}
48
+ title="Responsive Dialog"
49
+ >
50
+ <div className="space-y-4 py-4">
51
+ <p className="text-sm">
52
+ Try resizing your browser window or viewing this on different devices to see how it
53
+ adapts.
54
+ </p>
55
+ <p className="text-muted-foreground text-sm">
56
+ The component automatically switches between a centered dialog and a bottom drawer
57
+ based on the viewport width.
58
+ </p>
59
+ </div>
60
+ </ResponsiveDialog>
61
+ </>
62
+ );
63
+ },
64
+ };
65
+
66
+ export const WithForm: Story = {
67
+ args: {
68
+ open: false,
69
+ setOpen: () => {},
70
+ title: "",
71
+ },
72
+ render: () => {
73
+ const [open, setOpen] = useState(false);
74
+ const [email, setEmail] = useState("");
75
+
76
+ const handleSubmit = () => {
77
+ fn()(email);
78
+ setOpen(false);
79
+ setEmail("");
80
+ };
81
+
82
+ return (
83
+ <>
84
+ <Button onClick={() => setOpen(true)}>
85
+ <Mail size={16} />
86
+ Share Document
87
+ </Button>
88
+ <ResponsiveDialog
89
+ description="Enter the email address of the person you want to share this document with."
90
+ open={open}
91
+ setOpen={setOpen}
92
+ title="Share Document"
93
+ >
94
+ <div className="space-y-4 py-4">
95
+ <div className="space-y-2">
96
+ <label className="text-sm font-medium" htmlFor="email">
97
+ Email Address
98
+ </label>
99
+ <input
100
+ aria-label="Email Address"
101
+ className="border-input bg-background ring-offset-background focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none"
102
+ id="email"
103
+ onChange={(e) => setEmail(e.target.value)}
104
+ placeholder="colleague@example.com"
105
+ type="email"
106
+ value={email}
107
+ />
108
+ </div>
109
+ <div className="space-y-2">
110
+ <label className="text-sm font-medium" htmlFor="message">
111
+ Message (Optional)
112
+ </label>
113
+ <textarea
114
+ aria-label="Message (Optional)"
115
+ className="border-input bg-background ring-offset-background focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:outline-none"
116
+ id="message"
117
+ placeholder="Add a personal message..."
118
+ rows={3}
119
+ />
120
+ </div>
121
+ <div className="flex gap-2">
122
+ <Button className="flex-1" disabled={!email} onClick={handleSubmit}>
123
+ <Share2 size={16} />
124
+ Send Invitation
125
+ </Button>
126
+ </div>
127
+ </div>
128
+ </ResponsiveDialog>
129
+ </>
130
+ );
131
+ },
132
+ };
133
+
134
+ export const UploadFile: Story = {
135
+ args: {
136
+ open: false,
137
+ setOpen: () => {},
138
+ title: "",
139
+ },
140
+ render: () => {
141
+ const [open, setOpen] = useState(false);
142
+
143
+ return (
144
+ <>
145
+ <Button onClick={() => setOpen(true)}>
146
+ <Upload size={16} />
147
+ Upload File
148
+ </Button>
149
+ <ResponsiveDialog
150
+ description="Select a file to upload to your document library."
151
+ open={open}
152
+ setOpen={setOpen}
153
+ title="Upload Document"
154
+ >
155
+ <div className="space-y-4 py-4">
156
+ <div className="border-border bg-accent/50 rounded-lg border-2 border-dashed p-8 text-center">
157
+ <Upload className="text-muted-foreground mx-auto h-12 w-12" />
158
+ <p className="mt-2 text-sm font-medium">Drop files here or click to browse</p>
159
+ <p className="text-muted-foreground mt-1 text-xs">
160
+ Supports: PDF, DOCX, XLSX (Max 10MB)
161
+ </p>
162
+ <Button className="mt-4" size="sm" variant="outline">
163
+ Browse Files
164
+ </Button>
165
+ </div>
166
+ <div className="flex gap-2">
167
+ <Button className="flex-1" onClick={() => setOpen(false)}>
168
+ Start Upload
169
+ </Button>
170
+ </div>
171
+ </div>
172
+ </ResponsiveDialog>
173
+ </>
174
+ );
175
+ },
176
+ };
177
+
178
+ export const DeleteConfirmation: Story = {
179
+ args: {
180
+ open: false,
181
+ setOpen: () => {},
182
+ title: "",
183
+ },
184
+ render: () => {
185
+ const [open, setOpen] = useState(false);
186
+
187
+ const handleDelete = () => {
188
+ fn()();
189
+ setOpen(false);
190
+ };
191
+
192
+ return (
193
+ <>
194
+ <Button onClick={() => setOpen(true)} variant="destructive">
195
+ <Trash2 size={16} />
196
+ Delete Document
197
+ </Button>
198
+ <ResponsiveDialog
199
+ description="This action cannot be undone. This will permanently delete the document and remove all associated data."
200
+ open={open}
201
+ setOpen={setOpen}
202
+ title="Delete Document"
203
+ >
204
+ <div className="space-y-4 py-4">
205
+ <div className="border-destructive/50 bg-destructive/5 flex items-start gap-3 rounded-lg border p-4">
206
+ <AlertCircle className="text-destructive h-5 w-5 shrink-0" />
207
+ <div className="space-y-1">
208
+ <p className="text-sm font-medium">Warning</p>
209
+ <p className="text-muted-foreground text-sm">
210
+ Are you absolutely sure you want to delete this document? This action is
211
+ irreversible.
212
+ </p>
213
+ </div>
214
+ </div>
215
+ <div className="flex gap-2">
216
+ <Button className="flex-1" onClick={handleDelete} variant="destructive">
217
+ <Trash2 size={16} />
218
+ Delete Permanently
219
+ </Button>
220
+ </div>
221
+ </div>
222
+ </ResponsiveDialog>
223
+ </>
224
+ );
225
+ },
226
+ };
227
+
228
+ export const DocumentPreview: Story = {
229
+ args: {
230
+ open: false,
231
+ setOpen: () => {},
232
+ title: "",
233
+ },
234
+ render: () => {
235
+ const [open, setOpen] = useState(false);
236
+
237
+ return (
238
+ <>
239
+ <Button onClick={() => setOpen(true)} variant="outline">
240
+ <FileText size={16} />
241
+ View Details
242
+ </Button>
243
+ <ResponsiveDialog
244
+ description="View information about this document."
245
+ open={open}
246
+ setOpen={setOpen}
247
+ title="Document Details"
248
+ >
249
+ <div className="space-y-4 py-4">
250
+ <div className="space-y-3">
251
+ <div className="border-border bg-accent/50 flex justify-between rounded-lg border p-3">
252
+ <span className="text-sm font-medium">File Name</span>
253
+ <span className="text-muted-foreground text-sm">Annual_Report_2024.pdf</span>
254
+ </div>
255
+ <div className="border-border bg-accent/50 flex justify-between rounded-lg border p-3">
256
+ <span className="text-sm font-medium">Size</span>
257
+ <span className="text-muted-foreground text-sm">2.4 MB</span>
258
+ </div>
259
+ <div className="border-border bg-accent/50 flex justify-between rounded-lg border p-3">
260
+ <span className="text-sm font-medium">Last Modified</span>
261
+ <span className="text-muted-foreground text-sm">Dec 15, 2024</span>
262
+ </div>
263
+ <div className="border-border bg-accent/50 flex justify-between rounded-lg border p-3">
264
+ <span className="text-sm font-medium">Created By</span>
265
+ <span className="text-muted-foreground text-sm">John Doe</span>
266
+ </div>
267
+ </div>
268
+ <div className="flex gap-2">
269
+ <Button className="flex-1" onClick={() => setOpen(false)} variant="outline">
270
+ Download
271
+ </Button>
272
+ <Button className="flex-1" onClick={() => setOpen(false)}>
273
+ Open
274
+ </Button>
275
+ </div>
276
+ </div>
277
+ </ResponsiveDialog>
278
+ </>
279
+ );
280
+ },
281
+ };
282
+
283
+ export const NoDescription: Story = {
284
+ args: {
285
+ open: false,
286
+ setOpen: () => {},
287
+ title: "",
288
+ },
289
+ render: () => {
290
+ const [open, setOpen] = useState(false);
291
+
292
+ return (
293
+ <>
294
+ <Button onClick={() => setOpen(true)} variant="outline">
295
+ Open Simple Dialog
296
+ </Button>
297
+ <ResponsiveDialog open={open} setOpen={setOpen} title="Simple Dialog">
298
+ <div className="py-4">
299
+ <p className="text-sm">
300
+ This responsive dialog doesn't have a description, just a title and content.
301
+ </p>
302
+ </div>
303
+ </ResponsiveDialog>
304
+ </>
305
+ );
306
+ },
307
+ };
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import type { Dispatch, PropsWithChildren, SetStateAction } from "react";
5
+ import { useEffect } from "react";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/components/dialog/dialog";
13
+ import {
14
+ Drawer,
15
+ DrawerClose,
16
+ DrawerContent,
17
+ DrawerDescription,
18
+ DrawerFooter,
19
+ DrawerHeader,
20
+ DrawerTitle,
21
+ } from "@/components/drawer/drawer";
22
+ import { useMediaQuery } from "@/lib/hooks/use-media-query";
23
+ import { cn } from "@/lib/utils";
24
+ import { Button } from "../button/button";
25
+
26
+ type Props = PropsWithChildren & {
27
+ /** Extra classes for the desktop dialog panel (e.g. `sm:max-w-md`). */
28
+ className?: string;
29
+ description?: string;
30
+ /** Reset on open, e.g. a react-hook-form `useForm()` result. */
31
+ form?: { reset: () => void };
32
+ isPending?: boolean;
33
+ open: boolean;
34
+ setOpen: Dispatch<SetStateAction<boolean>>;
35
+ title?: string;
36
+ };
37
+
38
+ export function ResponsiveDialog({
39
+ children,
40
+ className,
41
+ description,
42
+ form,
43
+ isPending = false,
44
+ open,
45
+ setOpen,
46
+ title,
47
+ }: Props) {
48
+ const isDesktop = useMediaQuery("(min-width: 768px)");
49
+ const t = useTranslations("Common");
50
+
51
+ useEffect(() => {
52
+ if (open && form) {
53
+ form.reset();
54
+ }
55
+ }, [open, form]);
56
+
57
+ const handleOpenChange = (newOpen: boolean) => {
58
+ if (!newOpen && isPending) {
59
+ return;
60
+ }
61
+ setOpen(newOpen);
62
+ };
63
+
64
+ if (isDesktop) {
65
+ return (
66
+ <Dialog onOpenChange={handleOpenChange} open={open}>
67
+ <DialogContent aria-describedby={description ?? undefined} className={className}>
68
+ <DialogHeader>
69
+ <DialogTitle className={cn(!title && "sr-only")}>{title}</DialogTitle>
70
+ {description && <DialogDescription>{description}</DialogDescription>}
71
+ </DialogHeader>
72
+ {children}
73
+ </DialogContent>
74
+ </Dialog>
75
+ );
76
+ }
77
+
78
+ return (
79
+ <Drawer onOpenChange={handleOpenChange} open={open}>
80
+ <DrawerContent aria-describedby={description ?? undefined}>
81
+ <DrawerHeader className="text-left">
82
+ <DrawerTitle className={cn(!title && "sr-only")}>{title}</DrawerTitle>
83
+ {description && <DrawerDescription>{description}</DrawerDescription>}
84
+ </DrawerHeader>
85
+ <div className="px-5">{children}</div>
86
+ <DrawerFooter className="mx-1 pt-0.5">
87
+ <DrawerClose asChild>
88
+ <Button disabled={isPending} variant="secondary">
89
+ {t("cancel")}
90
+ </Button>
91
+ </DrawerClose>
92
+ </DrawerFooter>
93
+ </DrawerContent>
94
+ </Drawer>
95
+ );
96
+ }
@@ -0,0 +1,59 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { expect, userEvent, within } from "storybook/test";
3
+ import { Searchbar } from "./searchbar";
4
+
5
+ const meta = {
6
+ argTypes: {
7
+ disabled: {
8
+ control: "boolean",
9
+ description: "Disables the searchbar",
10
+ },
11
+ placeholder: {
12
+ control: "text",
13
+ description: "Placeholder text",
14
+ },
15
+ },
16
+ component: Searchbar,
17
+ parameters: {
18
+ layout: "centered",
19
+ },
20
+ tags: ["autodocs"],
21
+ } satisfies Meta<typeof Searchbar>;
22
+
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const Default: Story = {
27
+ args: {
28
+ className: "w-96",
29
+ placeholder: "Suche nach Dokumenten, Karteikarten...",
30
+ },
31
+ play: async ({ canvasElement }) => {
32
+ const canvas = within(canvasElement);
33
+ const field = canvas.getByRole("textbox");
34
+
35
+ await expect(canvas.queryByRole("button", { name: /clear/i })).not.toBeInTheDocument();
36
+
37
+ await userEvent.type(field, "thermodynamics");
38
+ await expect(field).toHaveValue("thermodynamics");
39
+
40
+ await userEvent.click(await canvas.findByRole("button", { name: /clear/i }));
41
+ await expect(field).toHaveValue("");
42
+ },
43
+ };
44
+
45
+ export const WithLabel: Story = {
46
+ args: {
47
+ className: "w-96",
48
+ label: "Suche",
49
+ placeholder: "Suche nach Dokumenten, Karteikarten...",
50
+ },
51
+ };
52
+
53
+ export const Disabled: Story = {
54
+ args: {
55
+ className: "w-96",
56
+ disabled: true,
57
+ placeholder: "Suche nach Dokumenten, Karteikarten...",
58
+ },
59
+ };
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { Search, X } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { type ComponentProps, useState } from "react";
6
+ import { Input } from "@/components/input/input";
7
+
8
+ export type SearchbarProps = Omit<
9
+ ComponentProps<typeof Input>,
10
+ "leadingIcon" | "trailingIcon" | "type"
11
+ > & {
12
+ onClear?: () => void;
13
+ };
14
+
15
+ export function Searchbar({ onChange, onClear, value, ...props }: SearchbarProps) {
16
+ const t = useTranslations("Common.a11y");
17
+ const [internalValue, setInternalValue] = useState("");
18
+ const currentValue = value ?? internalValue;
19
+
20
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
21
+ if (onChange) {
22
+ onChange(e);
23
+ } else {
24
+ setInternalValue(e.target.value);
25
+ }
26
+ };
27
+
28
+ const handleClear = () => {
29
+ if (onChange) {
30
+ const event = {
31
+ currentTarget: { value: "" },
32
+ target: { value: "" },
33
+ } as React.ChangeEvent<HTMLInputElement>;
34
+ onChange(event);
35
+ } else {
36
+ setInternalValue("");
37
+ }
38
+ onClear?.();
39
+ };
40
+
41
+ return (
42
+ <Input
43
+ {...props}
44
+ leadingIcon={<Search className="h-5 w-5" />}
45
+ onChange={handleChange}
46
+ trailingIcon={
47
+ currentValue ? (
48
+ <button
49
+ aria-label={t("clearSearch")}
50
+ className="hover:text-foreground flex cursor-pointer items-center justify-center transition-colors"
51
+ onClick={handleClear}
52
+ title={t("clearSearch")}
53
+ type="button"
54
+ >
55
+ <X className="h-5 w-5" />
56
+ </button>
57
+ ) : undefined
58
+ }
59
+ value={currentValue}
60
+ />
61
+ );
62
+ }
63
+
64
+ Searchbar.displayName = "Searchbar";