create-ncf 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 (44) hide show
  1. package/README.md +145 -0
  2. package/dist/index.js +813 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +28 -0
  5. package/templates/auth/src/app/(auth)/sign-in/page.tsx +99 -0
  6. package/templates/auth/src/app/(auth)/sign-up/page.tsx +118 -0
  7. package/templates/auth/src/app/api/auth/[...all]/route.ts +11 -0
  8. package/templates/auth/src/middleware.ts +74 -0
  9. package/templates/auth/src/server/auth/auth-client.ts +8 -0
  10. package/templates/auth/src/server/auth/auth.ts +77 -0
  11. package/templates/auth/src/server/db/schema/auth.ts +115 -0
  12. package/templates/base/biome.jsonc +68 -0
  13. package/templates/base/open-next.config.ts +3 -0
  14. package/templates/base/postcss.config.mjs +5 -0
  15. package/templates/base/src/app/layout.tsx +30 -0
  16. package/templates/base/src/app/page.tsx +24 -0
  17. package/templates/base/src/app/robots.ts +13 -0
  18. package/templates/base/src/app/sitemap.ts +12 -0
  19. package/templates/base/src/lib/utils.ts +6 -0
  20. package/templates/base/src/middleware.ts +9 -0
  21. package/templates/base/src/styles/globals.css +121 -0
  22. package/templates/base/tsconfig.json +37 -0
  23. package/templates/drizzle/drizzle.config.ts +7 -0
  24. package/templates/drizzle/migrations/.gitkeep +0 -0
  25. package/templates/drizzle/src/server/db/index.ts +9 -0
  26. package/templates/drizzle/src/server/db/schema/example.ts +15 -0
  27. package/templates/drizzle/src/server/db/schema/index.ts +1 -0
  28. package/templates/image-loader/src/lib/image-loader.ts +34 -0
  29. package/templates/posthog/instrumentation-client.ts +15 -0
  30. package/templates/queues/src/server/queues/handler.ts +43 -0
  31. package/templates/r2/src/server/services/storage.ts +53 -0
  32. package/templates/shadcn/components.json +22 -0
  33. package/templates/shadcn/src/components/ui/button.tsx +59 -0
  34. package/templates/shadcn/src/components/ui/card.tsx +92 -0
  35. package/templates/shadcn/src/components/ui/input.tsx +21 -0
  36. package/templates/shadcn/src/components/ui/skeleton.tsx +13 -0
  37. package/templates/shadcn/src/components/ui/sonner.tsx +28 -0
  38. package/templates/trpc/src/app/api/trpc/[trpc]/route.ts +29 -0
  39. package/templates/trpc/src/server/api/root.ts +10 -0
  40. package/templates/trpc/src/server/api/routes/example.ts +12 -0
  41. package/templates/trpc/src/server/api/trpc.ts +45 -0
  42. package/templates/trpc/src/server/api/trpc.with-auth.ts +67 -0
  43. package/templates/trpc/src/trpc/query-client.ts +23 -0
  44. package/templates/trpc/src/trpc/react.tsx +65 -0
@@ -0,0 +1,68 @@
1
+ {
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
+ "root": true,
4
+ "vcs": {
5
+ "enabled": true,
6
+ "useIgnoreFile": true,
7
+ "clientKind": "git"
8
+ },
9
+ "assist": {
10
+ "enabled": true,
11
+ "actions": {
12
+ "recommended": true,
13
+ "source": {
14
+ "recommended": true,
15
+ "organizeImports": "on",
16
+ "useSortedAttributes": "on"
17
+ }
18
+ }
19
+ },
20
+ "formatter": {
21
+ "enabled": true
22
+ },
23
+ "files": {
24
+ "includes": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "!!**/cloudflare-env.d.ts"]
25
+ },
26
+ "linter": {
27
+ "enabled": true,
28
+ "rules": {
29
+ "recommended": true,
30
+ "nursery": {
31
+ "useSortedClasses": {
32
+ "level": "warn",
33
+ "fix": "safe",
34
+ "options": {
35
+ "functions": ["clsx", "cva", "cn"]
36
+ }
37
+ }
38
+ },
39
+ "correctness": {
40
+ "noUnusedImports": {
41
+ "level": "error",
42
+ "fix": "safe"
43
+ },
44
+ "noUnusedVariables": {
45
+ "level": "error"
46
+ }
47
+ }
48
+ }
49
+ },
50
+ "javascript": {
51
+ "assist": { "enabled": true },
52
+ "formatter": { "enabled": true },
53
+ "linter": { "enabled": true }
54
+ },
55
+ "css": {
56
+ "assist": { "enabled": true },
57
+ "formatter": { "enabled": true },
58
+ "linter": { "enabled": true },
59
+ "parser": {
60
+ "cssModules": true,
61
+ "tailwindDirectives": true
62
+ }
63
+ },
64
+ "json": {
65
+ "formatter": { "enabled": true },
66
+ "linter": { "enabled": true }
67
+ }
68
+ }
@@ -0,0 +1,3 @@
1
+ import { defineCloudflareConfig } from "@opennextjs/cloudflare";
2
+
3
+ export default defineCloudflareConfig({});
@@ -0,0 +1,5 @@
1
+ const config = {
2
+ plugins: ["@tailwindcss/postcss"],
3
+ };
4
+
5
+ export default config;
@@ -0,0 +1,30 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter } from "next/font/google";
3
+ import "~/styles/globals.css";
4
+
5
+ const inter = Inter({
6
+ variable: "--font-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ export default function RootLayout({
11
+ children,
12
+ }: Readonly<{
13
+ children: React.ReactNode;
14
+ }>) {
15
+ return (
16
+ <html lang="en">
17
+ <body className={`${inter.variable} font-sans antialiased`}>
18
+ {children}
19
+ </body>
20
+ </html>
21
+ );
22
+ }
23
+
24
+ export const metadata: Metadata = {
25
+ title: {
26
+ default: "My App",
27
+ template: "%s | My App",
28
+ },
29
+ description: "Built with create-ncf",
30
+ };
@@ -0,0 +1,24 @@
1
+ export default function HomePage() {
2
+ return (
3
+ <main className="flex min-h-screen flex-col items-center justify-center">
4
+ <div className="space-y-4 text-center">
5
+ <h1 className="text-4xl font-bold tracking-tight">
6
+ Next.js + Cloudflare
7
+ </h1>
8
+ <p className="text-muted-foreground">
9
+ Scaffolded with{" "}
10
+ <code className="rounded bg-muted px-1.5 py-0.5 text-sm">
11
+ create-ncf
12
+ </code>
13
+ </p>
14
+ <p className="text-muted-foreground text-sm">
15
+ Edit{" "}
16
+ <code className="rounded bg-muted px-1.5 py-0.5 text-sm">
17
+ src/app/page.tsx
18
+ </code>{" "}
19
+ to get started.
20
+ </p>
21
+ </div>
22
+ </main>
23
+ );
24
+ }
@@ -0,0 +1,13 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ export default function robots(): MetadataRoute.Robots {
4
+ return {
5
+ rules: [
6
+ {
7
+ userAgent: "*",
8
+ allow: "/",
9
+ disallow: ["/api/"],
10
+ },
11
+ ],
12
+ };
13
+ }
@@ -0,0 +1,12 @@
1
+ import type { MetadataRoute } from "next";
2
+
3
+ export default function sitemap(): MetadataRoute.Sitemap {
4
+ return [
5
+ {
6
+ url: process.env.SITE_URL ?? "http://localhost:3000",
7
+ lastModified: new Date(),
8
+ changeFrequency: "daily",
9
+ priority: 1,
10
+ },
11
+ ];
12
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ export function middleware() {
4
+ return NextResponse.next();
5
+ }
6
+
7
+ export const config = {
8
+ matcher: [],
9
+ };
@@ -0,0 +1,121 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ @theme inline {
7
+ --color-background: var(--background);
8
+ --color-foreground: var(--foreground);
9
+ --font-sans: var(--font-sans);
10
+ --color-sidebar-ring: var(--sidebar-ring);
11
+ --color-sidebar-border: var(--sidebar-border);
12
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
13
+ --color-sidebar-accent: var(--sidebar-accent);
14
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
15
+ --color-sidebar-primary: var(--sidebar-primary);
16
+ --color-sidebar-foreground: var(--sidebar-foreground);
17
+ --color-sidebar: var(--sidebar);
18
+ --color-chart-5: var(--chart-5);
19
+ --color-chart-4: var(--chart-4);
20
+ --color-chart-3: var(--chart-3);
21
+ --color-chart-2: var(--chart-2);
22
+ --color-chart-1: var(--chart-1);
23
+ --color-ring: var(--ring);
24
+ --color-input: var(--input);
25
+ --color-border: var(--border);
26
+ --color-destructive: var(--destructive);
27
+ --color-accent-foreground: var(--accent-foreground);
28
+ --color-accent: var(--accent);
29
+ --color-muted-foreground: var(--muted-foreground);
30
+ --color-muted: var(--muted);
31
+ --color-secondary-foreground: var(--secondary-foreground);
32
+ --color-secondary: var(--secondary);
33
+ --color-primary-foreground: var(--primary-foreground);
34
+ --color-primary: var(--primary);
35
+ --color-popover-foreground: var(--popover-foreground);
36
+ --color-popover: var(--popover);
37
+ --color-card-foreground: var(--card-foreground);
38
+ --color-card: var(--card);
39
+ --radius-sm: calc(var(--radius) - 4px);
40
+ --radius-md: calc(var(--radius) - 2px);
41
+ --radius-lg: var(--radius);
42
+ --radius-xl: calc(var(--radius) + 4px);
43
+ }
44
+
45
+ :root {
46
+ --radius: 0.625rem;
47
+ --background: oklch(1 0 0);
48
+ --foreground: oklch(0.145 0 0);
49
+ --card: oklch(1 0 0);
50
+ --card-foreground: oklch(0.145 0 0);
51
+ --popover: oklch(1 0 0);
52
+ --popover-foreground: oklch(0.145 0 0);
53
+ --primary: oklch(0.205 0 0);
54
+ --primary-foreground: oklch(0.985 0 0);
55
+ --secondary: oklch(0.97 0 0);
56
+ --secondary-foreground: oklch(0.205 0 0);
57
+ --muted: oklch(0.97 0 0);
58
+ --muted-foreground: oklch(0.556 0 0);
59
+ --accent: oklch(0.97 0 0);
60
+ --accent-foreground: oklch(0.205 0 0);
61
+ --destructive: oklch(0.577 0.245 27.325);
62
+ --border: oklch(0.922 0 0);
63
+ --input: oklch(0.922 0 0);
64
+ --ring: oklch(0.708 0 0);
65
+ --chart-1: oklch(0.646 0.222 41.116);
66
+ --chart-2: oklch(0.6 0.118 184.704);
67
+ --chart-3: oklch(0.398 0.07 227.392);
68
+ --chart-4: oklch(0.828 0.189 84.429);
69
+ --chart-5: oklch(0.769 0.188 70.08);
70
+ --sidebar: oklch(0.985 0 0);
71
+ --sidebar-foreground: oklch(0.145 0 0);
72
+ --sidebar-primary: oklch(0.205 0 0);
73
+ --sidebar-primary-foreground: oklch(0.985 0 0);
74
+ --sidebar-accent: oklch(0.97 0 0);
75
+ --sidebar-accent-foreground: oklch(0.205 0 0);
76
+ --sidebar-border: oklch(0.922 0 0);
77
+ --sidebar-ring: oklch(0.708 0 0);
78
+ }
79
+
80
+ .dark {
81
+ --background: oklch(0.145 0 0);
82
+ --foreground: oklch(0.985 0 0);
83
+ --card: oklch(0.145 0 0);
84
+ --card-foreground: oklch(0.985 0 0);
85
+ --popover: oklch(0.145 0 0);
86
+ --popover-foreground: oklch(0.985 0 0);
87
+ --primary: oklch(0.985 0 0);
88
+ --primary-foreground: oklch(0.205 0 0);
89
+ --secondary: oklch(0.269 0 0);
90
+ --secondary-foreground: oklch(0.985 0 0);
91
+ --muted: oklch(0.269 0 0);
92
+ --muted-foreground: oklch(0.708 0 0);
93
+ --accent: oklch(0.269 0 0);
94
+ --accent-foreground: oklch(0.985 0 0);
95
+ --destructive: oklch(0.577 0.245 27.325);
96
+ --border: oklch(0.269 0 0);
97
+ --input: oklch(0.269 0 0);
98
+ --ring: oklch(0.439 0 0);
99
+ --chart-1: oklch(0.488 0.243 264.376);
100
+ --chart-2: oklch(0.696 0.17 162.48);
101
+ --chart-3: oklch(0.769 0.188 70.08);
102
+ --chart-4: oklch(0.627 0.265 303.9);
103
+ --chart-5: oklch(0.645 0.246 16.439);
104
+ --sidebar: oklch(0.205 0 0);
105
+ --sidebar-foreground: oklch(0.985 0 0);
106
+ --sidebar-primary: oklch(0.488 0.243 264.376);
107
+ --sidebar-primary-foreground: oklch(0.985 0 0);
108
+ --sidebar-accent: oklch(0.269 0 0);
109
+ --sidebar-accent-foreground: oklch(0.985 0 0);
110
+ --sidebar-border: oklch(1 0 0 / 10%);
111
+ --sidebar-ring: oklch(0.556 0 0);
112
+ }
113
+
114
+ @layer base {
115
+ * {
116
+ @apply border-border outline-ring/50;
117
+ }
118
+ body {
119
+ @apply bg-background text-foreground;
120
+ }
121
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "checkJs": true,
7
+ "skipLibCheck": true,
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "module": "esnext",
12
+ "moduleResolution": "bundler",
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "preserve",
16
+ "incremental": true,
17
+ "plugins": [
18
+ {
19
+ "name": "next"
20
+ }
21
+ ],
22
+ "paths": {
23
+ "~/*": ["./src/*"],
24
+ "lib.env": ["./lib.env.d.ts"]
25
+ },
26
+ "types": ["./cloudflare-env.d.ts", "node"]
27
+ },
28
+ "include": [
29
+ ".next/types/**/*.ts",
30
+ "next-env.d.ts",
31
+ "**/*.ts",
32
+ "**/*.tsx",
33
+ "**/*.cjs",
34
+ "**/*.js"
35
+ ],
36
+ "exclude": ["node_modules"]
37
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ dialect: "sqlite",
5
+ schema: "./src/server/db/schema",
6
+ out: "./migrations",
7
+ });
File without changes
@@ -0,0 +1,9 @@
1
+ import { getCloudflareContext } from "@opennextjs/cloudflare";
2
+ import { drizzle } from "drizzle-orm/d1";
3
+ import * as schema from "./schema";
4
+
5
+ export async function getDB() {
6
+ const { env } = await getCloudflareContext({ async: true });
7
+
8
+ return drizzle(env.DB, { schema });
9
+ }
@@ -0,0 +1,15 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
3
+
4
+ export const posts = sqliteTable("posts", {
5
+ id: integer("id").primaryKey({ autoIncrement: true }),
6
+ title: text("title").notNull(),
7
+ content: text("content"),
8
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
9
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
10
+ .notNull(),
11
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
12
+ .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
13
+ .$onUpdate(() => new Date())
14
+ .notNull(),
15
+ });
@@ -0,0 +1 @@
1
+ export * from "./example";
@@ -0,0 +1,34 @@
1
+ export default function cloudflareLoader({
2
+ src,
3
+ width,
4
+ quality,
5
+ }: {
6
+ src: string;
7
+ width: number;
8
+ quality?: number;
9
+ }) {
10
+ const params = [`width=${width}`];
11
+ if (quality) {
12
+ params.push(`quality=${quality}`);
13
+ }
14
+ params.push("format=auto");
15
+
16
+ const cdnDomain = process.env.NEXT_PUBLIC_CDN_DOMAIN;
17
+
18
+ try {
19
+ const url = new URL(src);
20
+
21
+ // Only apply Cloudflare Image Resizing to the configured CDN domain
22
+ if (cdnDomain) {
23
+ const cdnHostname = new URL(cdnDomain).hostname;
24
+ if (url.hostname === cdnHostname) {
25
+ return `${url.origin}/cdn-cgi/image/${params.join(",")}${url.pathname}`;
26
+ }
27
+ }
28
+
29
+ return src;
30
+ } catch {
31
+ // If src is not a valid URL (e.g., relative path), return as is
32
+ return src;
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ import posthog from "posthog-js";
2
+
3
+ const posthogKey = process.env.NEXT_PUBLIC_POSTHOG_KEY;
4
+ const posthogHost = process.env.NEXT_PUBLIC_POSTHOG_HOST;
5
+
6
+ if (posthogKey && posthogHost) {
7
+ posthog.init(posthogKey, {
8
+ api_host: posthogHost,
9
+ ui_host: "https://us.posthog.com",
10
+ capture_exceptions: true,
11
+ disable_surveys: true,
12
+ disable_session_recording: true,
13
+ debug: process.env.NODE_ENV === "development",
14
+ });
15
+ }
@@ -0,0 +1,43 @@
1
+ // Example queue message type — replace with your own
2
+ interface QueueMessage {
3
+ type: string;
4
+ payload: unknown;
5
+ }
6
+
7
+ export async function handleBatch(
8
+ batch: MessageBatch<unknown>,
9
+ env: CloudflareEnv,
10
+ ): Promise<void> {
11
+ for (const message of batch.messages) {
12
+ const body = message.body as QueueMessage;
13
+
14
+ try {
15
+ console.log(`Processing message: ${body.type}`, body.payload);
16
+
17
+ // TODO: Add your queue processing logic here
18
+
19
+ message.ack();
20
+ } catch (error) {
21
+ console.error(`Failed to process message: ${body.type}`, error);
22
+ message.retry();
23
+ }
24
+ }
25
+ }
26
+
27
+ export async function handleDlqBatch(
28
+ batch: MessageBatch<unknown>,
29
+ env: CloudflareEnv,
30
+ ): Promise<void> {
31
+ for (const message of batch.messages) {
32
+ const body = message.body as QueueMessage;
33
+
34
+ console.error(
35
+ `Dead letter queue message: ${body.type}`,
36
+ JSON.stringify(body.payload),
37
+ );
38
+
39
+ // TODO: Add dead letter queue handling (alerting, logging, etc.)
40
+
41
+ message.ack();
42
+ }
43
+ }
@@ -0,0 +1,53 @@
1
+ import { getCloudflareContext } from "@opennextjs/cloudflare";
2
+ import { env } from "~/env";
3
+
4
+ function normalizeKey(key: string): string {
5
+ return key.replace(/^\/+/, "");
6
+ }
7
+
8
+ async function resolveBucket(bucket?: R2Bucket): Promise<R2Bucket> {
9
+ if (bucket) return bucket;
10
+ const { env: cfEnv } = await getCloudflareContext({ async: true });
11
+ return cfEnv.STORAGE;
12
+ }
13
+
14
+ export function buildR2PublicUrl(key: string): string {
15
+ const normalized = normalizeKey(key);
16
+ const base = (env.NEXT_PUBLIC_R2_DOMAIN ?? "").replace(/\/+$/, "");
17
+ return `${base}/${normalized}`;
18
+ }
19
+
20
+ export async function uploadFile(
21
+ key: string,
22
+ data: ArrayBuffer,
23
+ contentType: string,
24
+ bucket?: R2Bucket,
25
+ ): Promise<{ key: string; url: string }> {
26
+ const storage = await resolveBucket(bucket);
27
+ const normalized = normalizeKey(key);
28
+
29
+ await storage.put(normalized, data, {
30
+ httpMetadata: { contentType },
31
+ });
32
+
33
+ return {
34
+ key: normalized,
35
+ url: buildR2PublicUrl(normalized),
36
+ };
37
+ }
38
+
39
+ export async function getFile(
40
+ key: string,
41
+ bucket?: R2Bucket,
42
+ ): Promise<R2ObjectBody | null> {
43
+ const storage = await resolveBucket(bucket);
44
+ return storage.get(normalizeKey(key));
45
+ }
46
+
47
+ export async function deleteFile(
48
+ key: string,
49
+ bucket?: R2Bucket,
50
+ ): Promise<void> {
51
+ const storage = await resolveBucket(bucket);
52
+ await storage.delete(normalizeKey(key));
53
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/styles/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "~/components",
16
+ "utils": "~/lib/utils",
17
+ "ui": "~/components/ui",
18
+ "lib": "~/lib",
19
+ "hooks": "~/hooks"
20
+ },
21
+ "registries": {}
22
+ }
@@ -0,0 +1,59 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "~/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost: "hover:bg-accent hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
24
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
25
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
26
+ icon: "size-9",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ size: "default",
32
+ },
33
+ }
34
+ )
35
+
36
+ function Button({
37
+ className,
38
+ variant = "default",
39
+ size = "default",
40
+ asChild = false,
41
+ ...props
42
+ }: React.ComponentProps<"button"> &
43
+ VariantProps<typeof buttonVariants> & {
44
+ asChild?: boolean
45
+ }) {
46
+ const Comp = asChild ? Slot.Root : "button"
47
+
48
+ return (
49
+ <Comp
50
+ data-slot="button"
51
+ data-variant={variant}
52
+ data-size={size}
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ export { Button, buttonVariants }