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.
- package/dist/index.js +1727 -0
- package/package.json +37 -0
- package/template/.agents/plugins/marketplace.json +21 -0
- package/template/.claude/settings.json +6 -0
- package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/template/.claude/skills/frontend-design/SKILL.md +154 -0
- package/template/.claude/skills/theme-factory/SKILL.md +95 -0
- package/template/.claude/skills/theme-factory/references/schema.md +104 -0
- package/template/.claude/skills/theme-factory/scripts/theme.mjs +261 -0
- package/template/.dockerignore +43 -0
- package/template/.editorconfig +10 -0
- package/template/.github/workflows/ci.yaml +289 -0
- package/template/.github/workflows/publish-app.yaml +66 -0
- package/template/.github/workflows/publish.yaml +148 -0
- package/template/.github/workflows/snyk.yaml +34 -0
- package/template/.vscode/settings.json +8 -0
- package/template/AGENTS.md +42 -0
- package/template/Dockerfile.base +9 -0
- package/template/README.md +54 -0
- package/template/_gitignore +10 -0
- package/template/apps/api/.env.example +32 -0
- package/template/apps/api/AGENTS.md +44 -0
- package/template/apps/api/Dockerfile +69 -0
- package/template/apps/api/_gitignore +8 -0
- package/template/apps/api/knip.config.ts +22 -0
- package/template/apps/api/kustomize/base/application.properties +16 -0
- package/template/apps/api/kustomize/base/deployment.yaml +94 -0
- package/template/apps/api/kustomize/base/kustomization.yaml +17 -0
- package/template/apps/api/kustomize/base/service.yaml +10 -0
- package/template/apps/api/kustomize/overlays/prod/kustomization.yaml +17 -0
- package/template/apps/api/kustomize/overlays/staging/application.properties +1 -0
- package/template/apps/api/kustomize/overlays/staging/kustomization.yaml +21 -0
- package/template/apps/api/kustomize/secret.keys +14 -0
- package/template/apps/api/oxfmt.config.ts +8 -0
- package/template/apps/api/oxlint.config.ts +107 -0
- package/template/apps/api/package.json +79 -0
- package/template/apps/api/prisma/migrations/20260927000000_init/migration.sql +113 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +113 -0
- package/template/apps/api/prisma.config.ts +7 -0
- package/template/apps/api/scripts/auth.cli.ts +19 -0
- package/template/apps/api/scripts/seal-secret.sh +171 -0
- package/template/apps/api/sonar-project.properties +9 -0
- package/template/apps/api/src/app-info/app-info.dto.ts +10 -0
- package/template/apps/api/src/app-info/app-info.module.ts +7 -0
- package/template/apps/api/src/app-info/app-info.resolver.spec.ts +14 -0
- package/template/apps/api/src/app-info/app-info.resolver.ts +21 -0
- package/template/apps/api/src/app.module.spec.ts +93 -0
- package/template/apps/api/src/app.module.ts +77 -0
- package/template/apps/api/src/auth/auth.guard.spec.ts +90 -0
- package/template/apps/api/src/auth/auth.guard.ts +97 -0
- package/template/apps/api/src/auth/auth.ts +129 -0
- package/template/apps/api/src/auth/decorator/allow-anonymous.decorator.ts +17 -0
- package/template/apps/api/src/auth/decorator/is-email-verified.decorator.spec.ts +41 -0
- package/template/apps/api/src/auth/decorator/is-email-verified.decorator.ts +20 -0
- package/template/apps/api/src/auth/decorator/roles.decorator.ts +27 -0
- package/template/apps/api/src/auth/testing/make-session.ts +31 -0
- package/template/apps/api/src/auth/types/auth.types.ts +27 -0
- package/template/apps/api/src/bootstrap/register-graphql-enums.ts +12 -0
- package/template/apps/api/src/bootstrap/security.ts +24 -0
- package/template/apps/api/src/bootstrap/swagger.ts +28 -0
- package/template/apps/api/src/config/app.config.spec.ts +22 -0
- package/template/apps/api/src/config/app.config.ts +122 -0
- package/template/apps/api/src/config/graphql-depth-limit.ts +103 -0
- package/template/apps/api/src/config/graphql.config.ts +77 -0
- package/template/apps/api/src/config/resources/app.yaml +29 -0
- package/template/apps/api/src/errors/app-parse-uuid.pipe.ts +27 -0
- package/template/apps/api/src/errors/app.exception.ts +47 -0
- package/template/apps/api/src/errors/error-code.enum.ts +54 -0
- package/template/apps/api/src/errors/http-status-fallback-code.util.ts +27 -0
- package/template/apps/api/src/errors/validation-exception.factory.ts +43 -0
- package/template/apps/api/src/health/health.controller.spec.ts +82 -0
- package/template/apps/api/src/health/health.controller.ts +57 -0
- package/template/apps/api/src/health/health.module.ts +12 -0
- package/template/apps/api/src/logging/context-logger.ts +18 -0
- package/template/apps/api/src/logging/logger.config.ts +23 -0
- package/template/apps/api/src/logging/logging.module.ts +22 -0
- package/template/apps/api/src/mail/mail.module.ts +12 -0
- package/template/apps/api/src/mail/mail.service.spec.ts +47 -0
- package/template/apps/api/src/mail/mail.service.ts +123 -0
- package/template/apps/api/src/main.ts +41 -0
- package/template/apps/api/src/prisma/prisma.module.ts +12 -0
- package/template/apps/api/src/prisma/prisma.service.spec.ts +136 -0
- package/template/apps/api/src/prisma/prisma.service.ts +138 -0
- package/template/apps/api/src/queue/dto/enqueue-example.input.ts +11 -0
- package/template/apps/api/src/queue/example-queue.resolver.ts +14 -0
- package/template/apps/api/src/queue/example-queue.service.spec.ts +21 -0
- package/template/apps/api/src/queue/example-queue.service.ts +32 -0
- package/template/apps/api/src/queue/queue.module.ts +27 -0
- package/template/apps/api/src/r2/r2.module.ts +12 -0
- package/template/apps/api/src/r2/r2.service.spec.ts +117 -0
- package/template/apps/api/src/r2/r2.service.ts +131 -0
- package/template/apps/api/src/user/dto/user.dto.ts +26 -0
- package/template/apps/api/src/user/user.module.ts +10 -0
- package/template/apps/api/src/user/user.resolver.spec.ts +18 -0
- package/template/apps/api/src/user/user.resolver.ts +24 -0
- package/template/apps/api/src/utils/catch-everything-exception.filter.spec.ts +292 -0
- package/template/apps/api/src/utils/catch-everything-exception.filter.ts +70 -0
- package/template/apps/api/src/utils/error-handler.util.spec.ts +20 -0
- package/template/apps/api/src/utils/error-handler.util.ts +12 -0
- package/template/apps/api/src/utils/escape-like-pattern.util.spec.ts +29 -0
- package/template/apps/api/src/utils/escape-like-pattern.util.ts +11 -0
- package/template/apps/api/src/utils/pagination.input.ts +20 -0
- package/template/apps/api/src/utils/prisma-exception.filter.spec.ts +186 -0
- package/template/apps/api/src/utils/prisma-exception.filter.ts +45 -0
- package/template/apps/api/src/utils/prisma-graphql-exception.filter.spec.ts +216 -0
- package/template/apps/api/src/utils/prisma-graphql-exception.filter.ts +43 -0
- package/template/apps/api/tests/app.e2e-spec.ts +35 -0
- package/template/apps/api/tests/auth.e2e-spec.ts +38 -0
- package/template/apps/api/tests/helpers/api.ts +27 -0
- package/template/apps/api/tsconfig.json +11 -0
- package/template/apps/api/vitest.config.e2e.ts +16 -0
- package/template/apps/api/vitest.config.ts +40 -0
- package/template/apps/web/.env.example +7 -0
- package/template/apps/web/.storybook/decorators.tsx +38 -0
- package/template/apps/web/.storybook/main.ts +17 -0
- package/template/apps/web/.storybook/preview.ts +38 -0
- package/template/apps/web/.storybook/vitest.setup.ts +5 -0
- package/template/apps/web/AGENTS.md +29 -0
- package/template/apps/web/Dockerfile +49 -0
- package/template/apps/web/_gitignore +16 -0
- package/template/apps/web/knip.config.ts +15 -0
- package/template/apps/web/kustomize/base/application.properties +5 -0
- package/template/apps/web/kustomize/base/deployment.yaml +60 -0
- package/template/apps/web/kustomize/base/kustomization.yaml +17 -0
- package/template/apps/web/kustomize/base/service.yaml +10 -0
- package/template/apps/web/kustomize/overlays/prod/kustomization.yaml +12 -0
- package/template/apps/web/kustomize/overlays/staging/application.properties +2 -0
- package/template/apps/web/kustomize/overlays/staging/kustomization.yaml +18 -0
- package/template/apps/web/messages/de.json +124 -0
- package/template/apps/web/messages/en.json +124 -0
- package/template/apps/web/next-env.d.ts +7 -0
- package/template/apps/web/next.config.ts +39 -0
- package/template/apps/web/oxfmt.config.ts +9 -0
- package/template/apps/web/oxlint.config.ts +36 -0
- package/template/apps/web/package.json +80 -0
- package/template/apps/web/playwright.config.ts +34 -0
- package/template/apps/web/postcss.config.mjs +7 -0
- package/template/apps/web/sonar-project.properties +9 -0
- package/template/apps/web/src/app/[locale]/(auth)/recovery/page.tsx +22 -0
- package/template/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +29 -0
- package/template/apps/web/src/app/[locale]/(auth)/sign-in/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/(auth)/sign-up/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/(auth)/verify/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/dashboard/page.tsx +34 -0
- package/template/apps/web/src/app/[locale]/layout.tsx +60 -0
- package/template/apps/web/src/app/[locale]/page.tsx +23 -0
- package/template/apps/web/src/app/api/health/route.ts +3 -0
- package/template/apps/web/src/app/global-error.tsx +19 -0
- package/template/apps/web/src/app/globals.css +98 -0
- package/template/apps/web/src/app/icon.svg +1 -0
- package/template/apps/web/src/app/robots.ts +15 -0
- package/template/apps/web/src/app/sitemap.ts +24 -0
- package/template/apps/web/src/components/alert/alert.stories.tsx +25 -0
- package/template/apps/web/src/components/alert/alert.tsx +50 -0
- package/template/apps/web/src/components/avatar/avatar.stories.tsx +116 -0
- package/template/apps/web/src/components/avatar/avatar.tsx +88 -0
- package/template/apps/web/src/components/button/button-variants.ts +28 -0
- package/template/apps/web/src/components/button/button.stories.tsx +33 -0
- package/template/apps/web/src/components/button/button.tsx +32 -0
- package/template/apps/web/src/components/checkbox/checkbox.stories.tsx +312 -0
- package/template/apps/web/src/components/checkbox/checkbox.tsx +38 -0
- package/template/apps/web/src/components/dialog/danger-panel.stories.tsx +41 -0
- package/template/apps/web/src/components/dialog/danger-panel.tsx +32 -0
- package/template/apps/web/src/components/dialog/dialog.stories.tsx +239 -0
- package/template/apps/web/src/components/dialog/dialog.tsx +136 -0
- package/template/apps/web/src/components/dialog-buttons.stories.tsx +55 -0
- package/template/apps/web/src/components/dialog-buttons.tsx +48 -0
- package/template/apps/web/src/components/drawer/drawer.stories.tsx +251 -0
- package/template/apps/web/src/components/drawer/drawer.tsx +120 -0
- package/template/apps/web/src/components/dropdown-menu/dropdown-menu.stories.tsx +329 -0
- package/template/apps/web/src/components/dropdown-menu/dropdown-menu.tsx +263 -0
- package/template/apps/web/src/components/empty-state.stories.tsx +49 -0
- package/template/apps/web/src/components/empty-state.tsx +33 -0
- package/template/apps/web/src/components/field-group.stories.tsx +47 -0
- package/template/apps/web/src/components/field-group.tsx +20 -0
- package/template/apps/web/src/components/field-label.stories.tsx +34 -0
- package/template/apps/web/src/components/field-label.tsx +28 -0
- package/template/apps/web/src/components/icons.tsx +25 -0
- package/template/apps/web/src/components/input/input-otp.stories.tsx +86 -0
- package/template/apps/web/src/components/input/input-otp.tsx +184 -0
- package/template/apps/web/src/components/input/input.stories.tsx +35 -0
- package/template/apps/web/src/components/input/input.tsx +180 -0
- package/template/apps/web/src/components/popover/popover.stories.tsx +479 -0
- package/template/apps/web/src/components/popover/popover.tsx +45 -0
- package/template/apps/web/src/components/responsive-dialog/responsive-dialog.stories.tsx +307 -0
- package/template/apps/web/src/components/responsive-dialog/responsive-dialog.tsx +96 -0
- package/template/apps/web/src/components/searchbar/searchbar.stories.tsx +59 -0
- package/template/apps/web/src/components/searchbar/searchbar.tsx +64 -0
- package/template/apps/web/src/components/select/select.stories.tsx +293 -0
- package/template/apps/web/src/components/select/select.tsx +445 -0
- package/template/apps/web/src/components/skeleton/skeleton.stories.tsx +76 -0
- package/template/apps/web/src/components/skeleton/skeleton.tsx +13 -0
- package/template/apps/web/src/components/spinner/spinner.tsx +18 -0
- package/template/apps/web/src/components/switch/switch.stories.tsx +71 -0
- package/template/apps/web/src/components/switch/switch.tsx +35 -0
- package/template/apps/web/src/components/tabs/tabs.stories.tsx +209 -0
- package/template/apps/web/src/components/tabs/tabs.tsx +69 -0
- package/template/apps/web/src/components/tabs/url-tabs.stories.tsx +68 -0
- package/template/apps/web/src/components/tabs/url-tabs.tsx +42 -0
- package/template/apps/web/src/components/textarea/textarea.stories.tsx +144 -0
- package/template/apps/web/src/components/textarea/textarea.tsx +115 -0
- package/template/apps/web/src/components/toast/toaster.tsx +38 -0
- package/template/apps/web/src/components/tooltip/tooltip.stories.tsx +237 -0
- package/template/apps/web/src/components/tooltip/tooltip.tsx +49 -0
- package/template/apps/web/src/config/csp.ts +59 -0
- package/template/apps/web/src/config/routes.ts +14 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/env.ts +15 -0
- package/template/apps/web/src/features/auth/auth-card.tsx +20 -0
- package/template/apps/web/src/features/auth/auth-divider.tsx +14 -0
- package/template/apps/web/src/features/auth/google-auth-button.tsx +49 -0
- package/template/apps/web/src/features/auth/google-one-tap.tsx +23 -0
- package/template/apps/web/src/features/auth/lib/auth-error.test.ts +22 -0
- package/template/apps/web/src/features/auth/lib/auth-error.ts +16 -0
- package/template/apps/web/src/features/auth/lib/return-to.test.ts +41 -0
- package/template/apps/web/src/features/auth/lib/return-to.ts +19 -0
- package/template/apps/web/src/features/auth/lib/validator.test.ts +45 -0
- package/template/apps/web/src/features/auth/lib/validator.ts +49 -0
- package/template/apps/web/src/features/auth/passkey-settings.tsx +106 -0
- package/template/apps/web/src/features/auth/recovery-form.tsx +61 -0
- package/template/apps/web/src/features/auth/resend-verification-button.tsx +36 -0
- package/template/apps/web/src/features/auth/reset-password-form.tsx +62 -0
- package/template/apps/web/src/features/auth/sign-in-form.stories.tsx +22 -0
- package/template/apps/web/src/features/auth/sign-in-form.tsx +116 -0
- package/template/apps/web/src/features/auth/sign-out-button.tsx +26 -0
- package/template/apps/web/src/features/auth/sign-up-form.tsx +107 -0
- package/template/apps/web/src/features/shared/provider.tsx +16 -0
- package/template/apps/web/src/features/shared/site-header.tsx +48 -0
- package/template/apps/web/src/features/shared/theme-toggle.tsx +24 -0
- package/template/apps/web/src/i18n/navigation.ts +5 -0
- package/template/apps/web/src/i18n/request.ts +21 -0
- package/template/apps/web/src/i18n/routing.ts +6 -0
- package/template/apps/web/src/lib/auth-client.ts +19 -0
- package/template/apps/web/src/lib/auth-server.ts +46 -0
- package/template/apps/web/src/lib/auth.ts +8 -0
- package/template/apps/web/src/lib/codegen.ts +30 -0
- package/template/apps/web/src/lib/graphql-client.ts +29 -0
- package/template/apps/web/src/lib/graphql-server.ts +28 -0
- package/template/apps/web/src/lib/graphql-utils.test.ts +32 -0
- package/template/apps/web/src/lib/graphql-utils.ts +76 -0
- package/template/apps/web/src/lib/hooks/use-media-query.ts +21 -0
- package/template/apps/web/src/lib/metadata.ts +17 -0
- package/template/apps/web/src/lib/network-error.test.ts +30 -0
- package/template/apps/web/src/lib/network-error.ts +17 -0
- package/template/apps/web/src/lib/proxy-utils.ts +42 -0
- package/template/apps/web/src/lib/query-client.ts +29 -0
- package/template/apps/web/src/lib/utils.ts +5 -0
- package/template/apps/web/src/proxy.ts +57 -0
- package/template/apps/web/test-results/.last-run.json +4 -0
- package/template/apps/web/tests/auth.spec.ts +17 -0
- package/template/apps/web/tests/csp-theme-script.spec.ts +37 -0
- package/template/apps/web/tests/home.spec.ts +25 -0
- package/template/apps/web/tsconfig.build.json +4 -0
- package/template/apps/web/tsconfig.json +28 -0
- package/template/apps/web/vitest.config.ts +40 -0
- package/template/apps/web/vitest.shims.d.ts +1 -0
- package/template/apps/worker/.env.example +4 -0
- package/template/apps/worker/AGENTS.md +11 -0
- package/template/apps/worker/Dockerfile +35 -0
- package/template/apps/worker/knip.config.ts +3 -0
- package/template/apps/worker/kustomize/base/application.properties +4 -0
- package/template/apps/worker/kustomize/base/deployment.yaml +62 -0
- package/template/apps/worker/kustomize/base/kustomization.yaml +16 -0
- package/template/apps/worker/kustomize/overlays/prod/kustomization.yaml +12 -0
- package/template/apps/worker/kustomize/overlays/staging/application.properties +1 -0
- package/template/apps/worker/kustomize/overlays/staging/kustomization.yaml +18 -0
- package/template/apps/worker/oxfmt.config.ts +8 -0
- package/template/apps/worker/oxlint.config.ts +15 -0
- package/template/apps/worker/package.json +33 -0
- package/template/apps/worker/sonar-project.properties +8 -0
- package/template/apps/worker/src/env.ts +13 -0
- package/template/apps/worker/src/lib/graceful-shutdown.ts +27 -0
- package/template/apps/worker/src/lib/logger.ts +20 -0
- package/template/apps/worker/src/main.ts +17 -0
- package/template/apps/worker/src/processors/example-processor.ts +15 -0
- package/template/apps/worker/src/worker/redis.ts +19 -0
- package/template/apps/worker/src/worker/worker.ts +37 -0
- package/template/apps/worker/tests/example-processor.test.ts +13 -0
- package/template/apps/worker/tests/graceful-shutdown.test.ts +34 -0
- package/template/apps/worker/tsconfig.json +4 -0
- package/template/apps/worker/vitest.config.ts +13 -0
- package/template/bun.lock +3053 -0
- package/template/bunfig.toml +2 -0
- package/template/compose.yaml +53 -0
- package/template/docker-bake.hcl +86 -0
- package/template/package.json +47 -0
- package/template/packages/lint/knip.ts +40 -0
- package/template/packages/lint/oxfmt.ts +8 -0
- package/template/packages/lint/oxlint.ts +136 -0
- package/template/packages/lint/package.json +13 -0
- package/template/packages/queues/oxfmt.config.ts +8 -0
- package/template/packages/queues/oxlint.config.ts +7 -0
- package/template/packages/queues/package.json +15 -0
- package/template/packages/queues/src/example-job.ts +11 -0
- package/template/packages/queues/src/index.ts +3 -0
- package/template/packages/queues/src/queue-names.ts +2 -0
- package/template/packages/queues/src/redis-connection.ts +27 -0
- package/template/packages/queues/tsconfig.json +4 -0
- package/template/packages/tsconfig/base.json +23 -0
- package/template/packages/tsconfig/node.json +11 -0
- package/template/packages/tsconfig/package.json +4 -0
- 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
|
+
}
|