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,263 @@
1
+ "use client";
2
+
3
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
4
+ import { Check, ChevronRight, Circle } from "lucide-react";
5
+ import type { ComponentPropsWithoutRef, ComponentRef, HTMLAttributes, RefObject } from "react";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ const DropdownMenu = DropdownMenuPrimitive.Root;
9
+
10
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
11
+
12
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
13
+
14
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
15
+
16
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
17
+
18
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
19
+
20
+ function DropdownMenuSubTrigger({
21
+ children,
22
+ className,
23
+ inset,
24
+ ref,
25
+ ...props
26
+ }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
27
+ inset?: boolean;
28
+ } & { ref?: RefObject<ComponentRef<typeof DropdownMenuPrimitive.SubTrigger> | null> }) {
29
+ return (
30
+ <DropdownMenuPrimitive.SubTrigger
31
+ className={cn(
32
+ "flex cursor-default select-none items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none",
33
+ "transition-colors duration-150",
34
+ "focus:bg-accent focus:text-accent-foreground",
35
+ "data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
36
+ "[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
37
+ inset && "pl-8",
38
+ className,
39
+ )}
40
+ ref={ref}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronRight className="ml-auto" />
45
+ </DropdownMenuPrimitive.SubTrigger>
46
+ );
47
+ }
48
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
49
+
50
+ function DropdownMenuSubContent({
51
+ className,
52
+ ref,
53
+ sideOffset = 8,
54
+ ...props
55
+ }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> & {
56
+ ref?: RefObject<ComponentRef<typeof DropdownMenuPrimitive.SubContent> | null>;
57
+ }) {
58
+ return (
59
+ <DropdownMenuPrimitive.SubContent
60
+ className={cn(
61
+ "z-100 min-w-32 overflow-hidden rounded-xl p-1.5",
62
+ "border-border bg-popover text-popover-foreground border shadow-lg shadow-black/8",
63
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
64
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
65
+ "origin-[--radix-dropdown-menu-content-transform-origin]",
66
+ className,
67
+ )}
68
+ ref={ref}
69
+ sideOffset={sideOffset}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
75
+
76
+ function DropdownMenuContent({
77
+ className,
78
+ ref,
79
+ sideOffset = 4,
80
+ ...props
81
+ }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
82
+ ref?: RefObject<ComponentRef<typeof DropdownMenuPrimitive.Content> | null>;
83
+ }) {
84
+ return (
85
+ <DropdownMenuPrimitive.Portal>
86
+ <DropdownMenuPrimitive.Content
87
+ className={cn(
88
+ "z-50 min-w-32 overflow-hidden rounded-xl p-1.5",
89
+ "border-border bg-popover text-popover-foreground border shadow-lg shadow-black/8",
90
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
91
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
92
+ "origin-[--radix-dropdown-menu-content-transform-origin]",
93
+ className,
94
+ )}
95
+ ref={ref}
96
+ sideOffset={sideOffset}
97
+ {...props}
98
+ />
99
+ </DropdownMenuPrimitive.Portal>
100
+ );
101
+ }
102
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
103
+
104
+ function DropdownMenuItem({
105
+ className,
106
+ destructive,
107
+ inset,
108
+ ref,
109
+ ...props
110
+ }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
111
+ destructive?: boolean;
112
+ inset?: boolean;
113
+ } & { ref?: RefObject<ComponentRef<typeof DropdownMenuPrimitive.Item> | null> }) {
114
+ return (
115
+ <DropdownMenuPrimitive.Item
116
+ className={cn(
117
+ "relative flex cursor-default select-none items-center gap-2 rounded-lg px-3 py-2 text-sm outline-none",
118
+ "transition-colors duration-150",
119
+ destructive
120
+ ? "text-destructive focus:bg-destructive/10 focus:text-destructive"
121
+ : "focus:bg-accent focus:text-accent-foreground",
122
+ "data-disabled:pointer-events-none data-disabled:opacity-50",
123
+ "[&>svg]:size-4 [&>svg]:shrink-0",
124
+ inset && "pl-8",
125
+ className,
126
+ )}
127
+ ref={ref}
128
+ {...props}
129
+ />
130
+ );
131
+ }
132
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
133
+
134
+ function DropdownMenuCheckboxItem({
135
+ checked,
136
+ children,
137
+ className,
138
+ ref,
139
+ ...props
140
+ }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & {
141
+ ref?: RefObject<ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem> | null>;
142
+ }) {
143
+ return (
144
+ <DropdownMenuPrimitive.CheckboxItem
145
+ checked={checked}
146
+ className={cn(
147
+ "relative flex cursor-default select-none items-center rounded-lg py-2 pr-3 pl-8 text-sm outline-none",
148
+ "transition-colors duration-150",
149
+ "focus:bg-accent focus:text-accent-foreground",
150
+ "data-disabled:pointer-events-none data-disabled:opacity-50",
151
+ className,
152
+ )}
153
+ ref={ref}
154
+ {...props}
155
+ >
156
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
157
+ <DropdownMenuPrimitive.ItemIndicator>
158
+ <Check className="text-primary size-4" />
159
+ </DropdownMenuPrimitive.ItemIndicator>
160
+ </span>
161
+ {children}
162
+ </DropdownMenuPrimitive.CheckboxItem>
163
+ );
164
+ }
165
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
166
+
167
+ function DropdownMenuRadioItem({
168
+ children,
169
+ className,
170
+ ref,
171
+ ...props
172
+ }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> & {
173
+ ref?: RefObject<ComponentRef<typeof DropdownMenuPrimitive.RadioItem> | null>;
174
+ }) {
175
+ return (
176
+ <DropdownMenuPrimitive.RadioItem
177
+ className={cn(
178
+ "relative flex cursor-default select-none items-center rounded-lg py-2 pr-3 pl-8 text-sm outline-none",
179
+ "transition-colors duration-150",
180
+ "focus:bg-accent focus:text-accent-foreground",
181
+ "data-disabled:pointer-events-none data-disabled:opacity-50",
182
+ className,
183
+ )}
184
+ ref={ref}
185
+ {...props}
186
+ >
187
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
188
+ <DropdownMenuPrimitive.ItemIndicator>
189
+ <Circle className="text-primary size-2 fill-current" />
190
+ </DropdownMenuPrimitive.ItemIndicator>
191
+ </span>
192
+ {children}
193
+ </DropdownMenuPrimitive.RadioItem>
194
+ );
195
+ }
196
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
197
+
198
+ function DropdownMenuLabel({
199
+ className,
200
+ inset,
201
+ ref,
202
+ ...props
203
+ }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
204
+ inset?: boolean;
205
+ } & { ref?: RefObject<ComponentRef<typeof DropdownMenuPrimitive.Label> | null> }) {
206
+ return (
207
+ <DropdownMenuPrimitive.Label
208
+ className={cn(
209
+ "text-muted-foreground px-3 py-1.5 text-xs font-medium",
210
+ inset && "pl-8",
211
+ className,
212
+ )}
213
+ ref={ref}
214
+ {...props}
215
+ />
216
+ );
217
+ }
218
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
219
+
220
+ function DropdownMenuSeparator({
221
+ className,
222
+ ref,
223
+ ...props
224
+ }: ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> & {
225
+ ref?: RefObject<ComponentRef<typeof DropdownMenuPrimitive.Separator> | null>;
226
+ }) {
227
+ return (
228
+ <DropdownMenuPrimitive.Separator
229
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
230
+ ref={ref}
231
+ {...props}
232
+ />
233
+ );
234
+ }
235
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
236
+
237
+ function DropdownMenuShortcut({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
238
+ return (
239
+ <span
240
+ className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
241
+ {...props}
242
+ />
243
+ );
244
+ }
245
+ DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
246
+
247
+ export {
248
+ DropdownMenu,
249
+ DropdownMenuCheckboxItem,
250
+ DropdownMenuContent,
251
+ DropdownMenuGroup,
252
+ DropdownMenuItem,
253
+ DropdownMenuLabel,
254
+ DropdownMenuPortal,
255
+ DropdownMenuRadioGroup,
256
+ DropdownMenuRadioItem,
257
+ DropdownMenuSeparator,
258
+ DropdownMenuShortcut,
259
+ DropdownMenuSub,
260
+ DropdownMenuSubContent,
261
+ DropdownMenuSubTrigger,
262
+ DropdownMenuTrigger,
263
+ };
@@ -0,0 +1,49 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { Inbox, SearchX } from "lucide-react";
3
+ import { expect, within } from "storybook/test";
4
+ import { Button } from "./button/button";
5
+ import { EmptyState } from "./empty-state";
6
+
7
+ const meta = {
8
+ args: { title: "No results found" },
9
+ component: EmptyState,
10
+ parameters: { layout: "centered" },
11
+ tags: ["autodocs"],
12
+ } satisfies Meta<typeof EmptyState>;
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const TextOnly: Story = {
19
+ args: { className: "gap-1 p-3", title: "No modules found" },
20
+ play: async ({ canvasElement }) => {
21
+ await expect(within(canvasElement).getByText("No modules found")).toBeInTheDocument();
22
+ },
23
+ };
24
+
25
+ export const WithIcon: Story = {
26
+ args: { icon: SearchX, title: "No results found" },
27
+ };
28
+
29
+ export const WithDescription: Story = {
30
+ args: {
31
+ description: "Try a different search term or remove some filters.",
32
+ icon: SearchX,
33
+ title: "Nothing matched your search",
34
+ },
35
+ };
36
+
37
+ export const WithAction: Story = {
38
+ args: {
39
+ action: <Button size="sm">Create your first deck</Button>,
40
+ description: "Card decks you create or save show up here.",
41
+ icon: Inbox,
42
+ title: "No card decks yet",
43
+ },
44
+ play: async ({ canvasElement }) => {
45
+ await expect(
46
+ within(canvasElement).getByRole("button", { name: "Create your first deck" }),
47
+ ).toBeInTheDocument();
48
+ },
49
+ };
@@ -0,0 +1,33 @@
1
+ import type { LucideIcon } from "lucide-react";
2
+ import type { ReactNode } from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ type EmptyStateProps = {
6
+ action?: ReactNode;
7
+ className?: string;
8
+ description?: string;
9
+ icon?: LucideIcon;
10
+ title: string;
11
+ };
12
+
13
+ /**
14
+ * Centered "nothing here" placeholder. Replaces the ad-hoc
15
+ * `<div class="text-muted-foreground text-center text-sm">…</div>` blocks that
16
+ * were copy-pasted across the search selectors and dropdowns.
17
+ */
18
+ export function EmptyState({ action, className, description, icon: Icon, title }: EmptyStateProps) {
19
+ return (
20
+ <div
21
+ className={cn("flex flex-col items-center justify-center gap-2 p-6 text-center", className)}
22
+ >
23
+ {Icon ? (
24
+ <div className="bg-muted rounded-xl p-3">
25
+ <Icon aria-hidden className="text-muted-foreground size-6" strokeWidth={1.5} />
26
+ </div>
27
+ ) : null}
28
+ <p className="text-foreground text-sm font-medium">{title}</p>
29
+ {description ? <p className="text-muted-foreground max-w-xs text-xs">{description}</p> : null}
30
+ {action}
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { expect, within } from "storybook/test";
3
+ import { withContainer } from "~sb/decorators";
4
+ import { Input } from "@/components/input/input";
5
+ import { FieldGroup } from "./field-group";
6
+
7
+ const meta = {
8
+ component: FieldGroup,
9
+ decorators: [withContainer("w-90")],
10
+ parameters: { layout: "centered" },
11
+ tags: ["autodocs"],
12
+ } satisfies Meta<typeof FieldGroup>;
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ const FRAMELESS =
19
+ "border-0 rounded-none bg-transparent shadow-none h-auto gap-0 px-0 focus-within:ring-0 focus-within:border-transparent";
20
+
21
+ export const TwoRows: Story = {
22
+ args: {
23
+ children: (
24
+ <>
25
+ <Input
26
+ aria-label="Title"
27
+ containerClassName="gap-y-0 px-3 pt-2.5 pb-1"
28
+ frameClassName={FRAMELESS}
29
+ placeholder="Title"
30
+ />
31
+ <Input
32
+ aria-label="Description"
33
+ containerClassName="gap-y-0 px-3 pt-1 pb-2.5"
34
+ frameClassName={FRAMELESS}
35
+ placeholder="Description"
36
+ size="sm"
37
+ />
38
+ </>
39
+ ),
40
+ },
41
+ play: async ({ canvasElement }) => {
42
+ const canvas = within(canvasElement);
43
+
44
+ await expect(canvas.getByRole("textbox", { name: "Title" })).toBeInTheDocument();
45
+ await expect(canvas.getByRole("textbox", { name: "Description" })).toBeInTheDocument();
46
+ },
47
+ };
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ /**
5
+ * Bordered container that visually merges a short stack of related fields into
6
+ * one control (title + description, module + professor). Each child is a row;
7
+ * rows carry their own padding (`px-3 py-2`) and are separated by a hairline.
8
+ */
9
+ export function FieldGroup({ children, className }: { children: ReactNode; className?: string }) {
10
+ return (
11
+ <div
12
+ className={cn(
13
+ "divide-border bg-card border-input divide-y overflow-hidden rounded-lg border shadow-xs",
14
+ className,
15
+ )}
16
+ >
17
+ {children}
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,34 @@
1
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
+ import { expect, within } from "storybook/test";
3
+ import { FieldLabel } from "./field-label";
4
+
5
+ const meta = {
6
+ args: {
7
+ children: "Email address",
8
+ htmlFor: "email",
9
+ required: false,
10
+ },
11
+ component: FieldLabel,
12
+ parameters: { layout: "centered" },
13
+ tags: ["autodocs"],
14
+ } satisfies Meta<typeof FieldLabel>;
15
+
16
+ export default meta;
17
+
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {
21
+ play: async ({ canvasElement }) => {
22
+ const label = within(canvasElement).getByText("Email address");
23
+
24
+ await expect(label).toHaveAttribute("for", "email");
25
+ await expect(within(canvasElement).queryByText("*")).not.toBeInTheDocument();
26
+ },
27
+ };
28
+
29
+ export const Required: Story = {
30
+ args: { required: true },
31
+ play: async ({ canvasElement }) => {
32
+ await expect(within(canvasElement).getByText("*")).toBeInTheDocument();
33
+ },
34
+ };
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ type Props = {
5
+ children: ReactNode;
6
+ className?: string;
7
+ htmlFor: string;
8
+ required?: boolean;
9
+ };
10
+
11
+ export function FieldLabel({ children, className, htmlFor, required }: Props) {
12
+ return (
13
+ <label
14
+ className={cn(
15
+ "text-foreground/90 ml-0.5 flex items-center gap-1 text-sm font-medium",
16
+ className,
17
+ )}
18
+ htmlFor={htmlFor}
19
+ >
20
+ {children}
21
+ {required && (
22
+ <span aria-hidden className="text-destructive">
23
+ *
24
+ </span>
25
+ )}
26
+ </label>
27
+ );
28
+ }
@@ -0,0 +1,25 @@
1
+ import type { SVGProps } from "react";
2
+
3
+ /** Brand glyphs lucide does not ship. */
4
+ export const Icons = {
5
+ Google: (props: SVGProps<SVGSVGElement>) => (
6
+ <svg aria-hidden viewBox="0 0 24 24" {...props}>
7
+ <path
8
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
9
+ fill="#4285F4"
10
+ />
11
+ <path
12
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
13
+ fill="#34A853"
14
+ />
15
+ <path
16
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
17
+ fill="#FBBC05"
18
+ />
19
+ <path
20
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
21
+ fill="#EA4335"
22
+ />
23
+ </svg>
24
+ ),
25
+ };
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
4
+ import { useState } from "react";
5
+ import { expect, userEvent, within } from "storybook/test";
6
+ import { InputOTP } from "./input-otp";
7
+
8
+ const meta: Meta<typeof InputOTP> = {
9
+ args: {
10
+ label: "Verification code",
11
+ length: 6,
12
+ value: "",
13
+ },
14
+ argTypes: {
15
+ disabled: {
16
+ control: "boolean",
17
+ },
18
+ error: {
19
+ control: "text",
20
+ },
21
+ hint: {
22
+ control: "text",
23
+ },
24
+ length: {
25
+ control: { max: 8, min: 3, step: 1, type: "number" },
26
+ },
27
+ value: {
28
+ control: false,
29
+ },
30
+ },
31
+ component: InputOTP,
32
+ parameters: {
33
+ layout: "centered",
34
+ },
35
+ tags: ["autodocs"],
36
+ } satisfies Meta<typeof InputOTP>;
37
+
38
+ export default meta;
39
+ type Story = StoryObj<typeof meta>;
40
+
41
+ export const Default: Story = {
42
+ play: async ({ canvasElement }) => {
43
+ const canvas = within(canvasElement);
44
+ const cells = canvas.getAllByRole("textbox");
45
+
46
+ await expect(cells).toHaveLength(6);
47
+
48
+ await userEvent.type(cells[0], "4");
49
+ await expect(cells[0]).toHaveValue("4");
50
+ },
51
+ render: (args) => {
52
+ const [code, setCode] = useState(args.value);
53
+
54
+ return (
55
+ <InputOTP
56
+ {...args}
57
+ onChange={(next) => {
58
+ setCode(next);
59
+ args.onChange?.(next);
60
+ }}
61
+ onComplete={(next) => {
62
+ args.onComplete?.(next);
63
+ }}
64
+ value={code}
65
+ />
66
+ );
67
+ },
68
+ };
69
+
70
+ export const WithHint: Story = {
71
+ args: {
72
+ hint: "Enter the 6-digit code we emailed you.",
73
+ },
74
+ };
75
+
76
+ export const WithError: Story = {
77
+ args: {
78
+ error: "The code you entered is invalid.",
79
+ },
80
+ };
81
+
82
+ export const Disabled: Story = {
83
+ args: {
84
+ disabled: true,
85
+ },
86
+ };