-
PROJECT
+
PROJECT_VITE
-
setCount((count) => count + 1)}>
+ setCount((count) => count + 1)} type="button">
count is {count}
@@ -21,7 +21,7 @@ function App() {
>
- )
+ );
}
-export default App
+export default App;
diff --git a/templates/vite/src/index.css b/templates/apps/vite/src/index.css
similarity index 100%
rename from templates/vite/src/index.css
rename to templates/apps/vite/src/index.css
diff --git a/templates/apps/vite/src/main.tsx b/templates/apps/vite/src/main.tsx
new file mode 100644
index 00000000..8c4462a7
--- /dev/null
+++ b/templates/apps/vite/src/main.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App.tsx';
+import './index.css';
+
+ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
+
+
+ ,
+);
diff --git a/templates/vite/tsconfig.json b/templates/apps/vite/tsconfig.json
similarity index 87%
rename from templates/vite/tsconfig.json
rename to templates/apps/vite/tsconfig.json
index ff268441..557ffa0d 100644
--- a/templates/vite/tsconfig.json
+++ b/templates/apps/vite/tsconfig.json
@@ -19,6 +19,5 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
- "include": ["src"],
- "references": [{ "path": "./tsconfig.node.json" }]
+ "include": ["src"]
}
diff --git a/templates/vite/tsconfig.node.json b/templates/apps/vite/tsconfig.node.json
similarity index 91%
rename from templates/vite/tsconfig.node.json
rename to templates/apps/vite/tsconfig.node.json
index f996e21f..76e733fe 100644
--- a/templates/vite/tsconfig.node.json
+++ b/templates/apps/vite/tsconfig.node.json
@@ -3,6 +3,7 @@
"compilerOptions": {
"composite": true,
"module": "ESNext",
+ "noEmit": false,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
diff --git a/templates/vite/vite.config.ts b/templates/apps/vite/vite.config.ts
similarity index 100%
rename from templates/vite/vite.config.ts
rename to templates/apps/vite/vite.config.ts
diff --git a/templates/next/README.md b/templates/next/README.md
deleted file mode 100644
index e215bc4c..00000000
--- a/templates/next/README.md
+++ /dev/null
@@ -1,36 +0,0 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
-
-## Getting Started
-
-First, run the development server:
-
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
-```
-
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
-
-## Deploy on Vercel
-
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
-
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/templates/next/src/app/layout.tsx b/templates/next/src/app/layout.tsx
deleted file mode 100644
index 4dfb480f..00000000
--- a/templates/next/src/app/layout.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import type { Metadata } from "next";
-import { Geist, Geist_Mono, Urbanist } from "next/font/google";
-import "./globals.css";
-import { Providers } from "@/app/providers";
-import { withAuth } from "@repo/auth/server";
-import { getFeatureFlags } from "@repo/analytics/server";
-import Header from "@/components/header";
-import Footer from "@/components/footer";
-import { getUrl } from "@repo/utils";
-
-const geistSans = Geist({
- variable: "--font-sans",
- subsets: ["latin"],
-});
-
-const urbanistSerif = Urbanist({
- variable: "--font-serif",
- subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
- variable: "--font-mono",
- subsets: ["latin"],
-});
-
-export const metadata: Metadata = {
- title: "StartupKit Next Template",
- description: "",
- metadataBase: new URL(getUrl()),
- openGraph: {
- title: "StartupKit Next Template",
- description: "",
- url: getUrl(),
- siteName: 'StartupKit Next Template',
- images: [
- {
- url: '/hero/og.avif',
- width: 1200,
- height: 630,
- alt: 'StartupKit Next Template',
- },
- ],
- locale: 'en_US',
- type: 'website',
- },
- twitter: {
- card: 'summary_large_image',
- title: "StartupKit Next Template",
- description: "",
- images: ['/hero/og.avif'],
- },
- icons: {
- icon: '/favicon.ico',
- shortcut: '/favicon-16x16.png',
- apple: '/apple-touch-icon.png',
- },
-};
-
-export const viewport = {
- width: "device-width",
- initialScale: 1,
- maximumScale: 1,
- userScalable: "no",
-};
-
-export default async function RootLayout({
- children,
-}: Readonly<{
- children: React.ReactNode;
-}>) {
- const { user } = await withAuth();
- const flags = await getFeatureFlags(user?.id);
-
- return (
-
-
-
-
-
- );
-}
diff --git a/templates/next/src/app/page.tsx b/templates/next/src/app/page.tsx
deleted file mode 100644
index 91f433ca..00000000
--- a/templates/next/src/app/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function Home() {
- return (
- <>
- Welcome to StartupKit
- >
- );
-}
diff --git a/templates/next/src/app/providers.tsx b/templates/next/src/app/providers.tsx
deleted file mode 100644
index 3e9b94fd..00000000
--- a/templates/next/src/app/providers.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-"use client";
-
-import { useRouter, useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
-import { UIProvider } from "@repo/ui/providers"
-import { AnalyticsProvider } from "@repo/analytics";
-import type { Flags } from "@repo/analytics/server";
-import { toast } from "@repo/ui/components/toast"
-import { AuthProvider, User } from "@repo/auth";
-
-export function Providers({
- children,
- flags,
- user
-}: {
- children: React.ReactNode;
- flags: Flags;
- user?: User
-}) {
- const { replace, refresh } = useRouter();
- const searchParams = useSearchParams();
-
- const params = useMemo(
- () => Object.fromEntries(searchParams.entries()),
- [searchParams],
- );
-
- useEffect(() => {
- const { error, ...queryParams } = params;
-
- if (error) {
- const newQueryParams = new URLSearchParams(
- queryParams as Record
,
- ).toString();
- toast.error(getErrorMessage(error));
- const newUrl = `${window.location.pathname}${newQueryParams ? `?${newQueryParams}` : ""}`;
- replace(newUrl);
- }
- }, [replace, params]);
-
- return (
-
-
- {children}
-
-
- );
-}
-
-const getErrorMessage = (error: string) => {
- if (error === "AUTH_FAILED") {
- return "Authentication failed. Please try again.";
- }
- return error;
-};
diff --git a/templates/next/src/components/header.tsx b/templates/next/src/components/header.tsx
deleted file mode 100644
index c49ef3e3..00000000
--- a/templates/next/src/components/header.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { Container } from "@/components/container"
-import { Logo } from "@repo/ui/components/logo"
-import { ButtonLink } from "@repo/ui/components/button"
-import { useAuth } from "@repo/auth"
-import { getUrl } from "@repo/utils"
-
-export default function Header() {
- const [isMenuOpen, setIsMenuOpen] = useState(false)
- const { isAuthenticated } = useAuth()
-
- return (
-
- )
-}
diff --git a/templates/next/tsconfig.json b/templates/next/tsconfig.json
deleted file mode 100644
index de7c16e5..00000000
--- a/templates/next/tsconfig.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "extends": "@repo/tsconfig/nextjs.json",
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"],
- "@repo/ui/*": ["../../packages/ui/src/*"]
- }
- },
- "include": [
- ".next/types/**/*.ts",
- "next-env.d.ts",
- "src/**/*.ts",
- "src/**/*.tsx"
- ],
- "exclude": ["node_modules", ".next"]
-}
diff --git a/templates/package/package.json b/templates/package/package.json
index cb366ed5..da0cdd3e 100644
--- a/templates/package/package.json
+++ b/templates/package/package.json
@@ -19,8 +19,8 @@
"@repo/utils": "workspace:*"
},
"devDependencies": {
- "@repo/biome": "workspace:*",
- "@repo/tsconfig": "workspace:*",
+ "@config/biome": "workspace:*",
+ "@config/tsconfig": "workspace:*",
"@types/node": "catalog:stack"
}
}
diff --git a/templates/repo/packages/analytics/README.md b/templates/packages/analytics/README.md
similarity index 100%
rename from templates/repo/packages/analytics/README.md
rename to templates/packages/analytics/README.md
diff --git a/templates/repo/packages/analytics/biome.jsonc b/templates/packages/analytics/biome.jsonc
similarity index 100%
rename from templates/repo/packages/analytics/biome.jsonc
rename to templates/packages/analytics/biome.jsonc
diff --git a/templates/repo/packages/analytics/package.json b/templates/packages/analytics/package.json
similarity index 100%
rename from templates/repo/packages/analytics/package.json
rename to templates/packages/analytics/package.json
diff --git a/templates/repo/packages/analytics/src/components/analytics-provider.tsx b/templates/packages/analytics/src/components/analytics-provider.tsx
similarity index 100%
rename from templates/repo/packages/analytics/src/components/analytics-provider.tsx
rename to templates/packages/analytics/src/components/analytics-provider.tsx
diff --git a/templates/repo/packages/analytics/src/hooks/use-flag.ts b/templates/packages/analytics/src/hooks/use-flag.ts
similarity index 100%
rename from templates/repo/packages/analytics/src/hooks/use-flag.ts
rename to templates/packages/analytics/src/hooks/use-flag.ts
diff --git a/templates/repo/packages/analytics/src/index.ts b/templates/packages/analytics/src/index.ts
similarity index 100%
rename from templates/repo/packages/analytics/src/index.ts
rename to templates/packages/analytics/src/index.ts
diff --git a/templates/repo/packages/analytics/src/server.ts b/templates/packages/analytics/src/server.ts
similarity index 100%
rename from templates/repo/packages/analytics/src/server.ts
rename to templates/packages/analytics/src/server.ts
diff --git a/templates/repo/packages/analytics/src/types.ts b/templates/packages/analytics/src/types.ts
similarity index 100%
rename from templates/repo/packages/analytics/src/types.ts
rename to templates/packages/analytics/src/types.ts
diff --git a/templates/repo/packages/analytics/src/vendor/posthog.ts b/templates/packages/analytics/src/vendor/posthog.ts
similarity index 100%
rename from templates/repo/packages/analytics/src/vendor/posthog.ts
rename to templates/packages/analytics/src/vendor/posthog.ts
diff --git a/templates/repo/packages/analytics/tsconfig.json b/templates/packages/analytics/tsconfig.json
similarity index 100%
rename from templates/repo/packages/analytics/tsconfig.json
rename to templates/packages/analytics/tsconfig.json
diff --git a/templates/repo/packages/auth/README.md b/templates/packages/auth/README.md
similarity index 100%
rename from templates/repo/packages/auth/README.md
rename to templates/packages/auth/README.md
diff --git a/templates/repo/packages/auth/biome.jsonc b/templates/packages/auth/biome.jsonc
similarity index 100%
rename from templates/repo/packages/auth/biome.jsonc
rename to templates/packages/auth/biome.jsonc
diff --git a/templates/repo/packages/auth/package.json b/templates/packages/auth/package.json
similarity index 100%
rename from templates/repo/packages/auth/package.json
rename to templates/packages/auth/package.json
diff --git a/templates/packages/auth/src/components/index.ts b/templates/packages/auth/src/components/index.ts
new file mode 100644
index 00000000..de7133f6
--- /dev/null
+++ b/templates/packages/auth/src/components/index.ts
@@ -0,0 +1,2 @@
+export { useAuth } from "../hooks/use-auth"
+export { AuthProvider } from "./provider"
diff --git a/templates/repo/packages/auth/src/components/provider.tsx b/templates/packages/auth/src/components/provider.tsx
similarity index 100%
rename from templates/repo/packages/auth/src/components/provider.tsx
rename to templates/packages/auth/src/components/provider.tsx
diff --git a/templates/packages/auth/src/hooks/use-auth.ts b/templates/packages/auth/src/hooks/use-auth.ts
new file mode 100644
index 00000000..46fe1209
--- /dev/null
+++ b/templates/packages/auth/src/hooks/use-auth.ts
@@ -0,0 +1,18 @@
+"use client"
+
+import { useAuth as useStartupKitAuth } from "@startupkit/auth"
+import type { User } from "../types"
+
+interface UseAuthReturn {
+ isAuthenticated: boolean
+ isLoading: boolean
+ user: User | null | undefined
+ logout: () => Promise
+ sendAuthCode: (email: string) => Promise
+ verifyAuthCode: (email: string, otp: string) => Promise
+ googleAuth: () => Promise
+}
+
+export function useAuth(): UseAuthReturn {
+ return useStartupKitAuth() as UseAuthReturn
+}
diff --git a/templates/repo/packages/auth/src/index.ts b/templates/packages/auth/src/index.ts
similarity index 100%
rename from templates/repo/packages/auth/src/index.ts
rename to templates/packages/auth/src/index.ts
diff --git a/templates/repo/packages/auth/src/lib/auth.ts b/templates/packages/auth/src/lib/auth.ts
similarity index 62%
rename from templates/repo/packages/auth/src/lib/auth.ts
rename to templates/packages/auth/src/lib/auth.ts
index 3e6e4741..2d5ba53c 100644
--- a/templates/repo/packages/auth/src/lib/auth.ts
+++ b/templates/packages/auth/src/lib/auth.ts
@@ -1,5 +1,5 @@
import { track } from "@repo/analytics/server"
-import { db, users } from "@repo/db"
+import * as dbSchema from "@repo/db"
import { sendEmail } from "@repo/emails"
import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle"
@@ -26,8 +26,26 @@ async function sendVerificationOTP({
export const auth = betterAuth({
basePath: "/auth",
- database: drizzleAdapter(db, {
- provider: "pg"
+ advanced: {
+ generateId: () => crypto.randomUUID()
+ },
+ logger: {
+ disabled: false,
+ disableColors: false,
+ level: "error",
+ log: (level, message, ...args) => {
+ // Custom logging implementation
+ console.log(`[${level}] ${message}`, ...args)
+ }
+ },
+ database: drizzleAdapter(dbSchema.db, {
+ provider: "pg",
+ schema: {
+ user: dbSchema.users,
+ account: dbSchema.accounts,
+ session: dbSchema.sessions,
+ verification: dbSchema.verifications
+ }
}),
account: {
accountLinking: {
@@ -55,22 +73,22 @@ export const auth = betterAuth({
const { newSession, newUser } = ctx.context
if (newSession) {
- await db
- .update(users)
+ await dbSchema.db
+ .update(dbSchema.users)
.set({
lastSeenAt: new Date()
- } as unknown as typeof users.$inferInsert)
- .where(eq(users.id, newSession.user.id))
+ } as unknown as typeof dbSchema.users.$inferInsert)
+ .where(eq(dbSchema.users.id, newSession.user.id))
- const [user] = await db
+ const [user] = await dbSchema.db
.select({
- id: users.id,
- email: users.email,
- firstName: users.firstName,
- lastName: users.lastName
+ id: dbSchema.users.id,
+ email: dbSchema.users.email,
+ firstName: dbSchema.users.firstName,
+ lastName: dbSchema.users.lastName
})
- .from(users)
- .where(eq(users.id, newSession.user.id))
+ .from(dbSchema.users)
+ .where(eq(dbSchema.users.id, newSession.user.id))
.limit(1)
if (user?.email) {
@@ -99,8 +117,11 @@ export const auth = betterAuth({
},
socialProviders: {
google: {
- clientId: process.env.GOOGLE_CLIENT_ID as string,
- clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
+ clientId: process.env.GOOGLE_CLIENT_ID || "",
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
+ enabled: Boolean(
+ process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
+ ),
mapProfileToUser: async (profile) => {
return {
name: `${profile.given_name} ${profile.family_name}`,
diff --git a/templates/repo/packages/auth/src/server.ts b/templates/packages/auth/src/server.ts
similarity index 82%
rename from templates/repo/packages/auth/src/server.ts
rename to templates/packages/auth/src/server.ts
index f2e87a94..d97e2ef4 100644
--- a/templates/repo/packages/auth/src/server.ts
+++ b/templates/packages/auth/src/server.ts
@@ -1,4 +1,5 @@
import { getFeatureFlags } from "@repo/analytics/server"
+import { toNextJsHandler } from "better-auth/next-js"
import { headers } from "next/headers"
import { auth } from "./lib/auth"
@@ -7,7 +8,9 @@ export { auth }
export type Session = typeof auth.$Infer.Session.session
export type User = typeof auth.$Infer.Session.user
-export const handler = auth.handler
+export function handler() {
+ return toNextJsHandler(auth.handler)
+}
export async function withAuth(opts?: { flags?: boolean }) {
const session = await auth.api.getSession({
diff --git a/templates/repo/packages/auth/src/types.ts b/templates/packages/auth/src/types.ts
similarity index 100%
rename from templates/repo/packages/auth/src/types.ts
rename to templates/packages/auth/src/types.ts
diff --git a/templates/repo/packages/auth/tsconfig.json b/templates/packages/auth/tsconfig.json
similarity index 100%
rename from templates/repo/packages/auth/tsconfig.json
rename to templates/packages/auth/tsconfig.json
diff --git a/templates/repo/packages/db/biome.jsonc b/templates/packages/db/biome.jsonc
similarity index 100%
rename from templates/repo/packages/db/biome.jsonc
rename to templates/packages/db/biome.jsonc
diff --git a/templates/repo/packages/db/drizzle.config.ts b/templates/packages/db/drizzle.config.ts
similarity index 100%
rename from templates/repo/packages/db/drizzle.config.ts
rename to templates/packages/db/drizzle.config.ts
diff --git a/templates/packages/db/drizzle/0000_initial.sql b/templates/packages/db/drizzle/0000_initial.sql
new file mode 100644
index 00000000..b817027f
--- /dev/null
+++ b/templates/packages/db/drizzle/0000_initial.sql
@@ -0,0 +1,86 @@
+CREATE TYPE "public"."team_role" AS ENUM('owner', 'member');--> statement-breakpoint
+CREATE TABLE "Account" (
+ "id" text PRIMARY KEY NOT NULL,
+ "userId" text NOT NULL,
+ "accountId" text NOT NULL,
+ "providerId" text NOT NULL,
+ "accessToken" text,
+ "refreshToken" text,
+ "accessTokenExpiresAt" timestamp,
+ "refreshTokenExpiresAt" timestamp,
+ "scope" text,
+ "idToken" text,
+ "password" text,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "Session" (
+ "id" text PRIMARY KEY NOT NULL,
+ "userId" text NOT NULL,
+ "token" text NOT NULL,
+ "expiresAt" timestamp NOT NULL,
+ "ipAddress" text,
+ "userAgent" text,
+ "impersonatedBy" text,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL,
+ CONSTRAINT "Session_token_unique" UNIQUE("token")
+);
+--> statement-breakpoint
+CREATE TABLE "TeamMember" (
+ "teamId" text NOT NULL,
+ "userId" text NOT NULL,
+ "role" "team_role" DEFAULT 'member' NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp,
+ "joinedAt" timestamp
+);
+--> statement-breakpoint
+CREATE TABLE "Team" (
+ "id" text PRIMARY KEY NOT NULL,
+ "name" varchar(64) NOT NULL,
+ "slug" text NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp,
+ CONSTRAINT "Team_slug_unique" UNIQUE("slug")
+);
+--> statement-breakpoint
+CREATE TABLE "User" (
+ "id" text PRIMARY KEY NOT NULL,
+ "firstName" text,
+ "lastName" text,
+ "name" text,
+ "image" text,
+ "email" text,
+ "emailVerified" boolean DEFAULT false NOT NULL,
+ "phone" text,
+ "role" text DEFAULT 'user' NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL,
+ "lastSeenAt" timestamp,
+ CONSTRAINT "User_email_unique" UNIQUE("email"),
+ CONSTRAINT "User_phone_unique" UNIQUE("phone")
+);
+--> statement-breakpoint
+CREATE TABLE "Verification" (
+ "id" text PRIMARY KEY NOT NULL,
+ "identifier" text NOT NULL,
+ "value" text NOT NULL,
+ "expiresAt" timestamp NOT NULL,
+ "createdAt" timestamp DEFAULT now() NOT NULL,
+ "updatedAt" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamId_Team_id_fk" FOREIGN KEY ("teamId") REFERENCES "public"."Team"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "Account_userId_idx" ON "Account" USING btree ("userId");--> statement-breakpoint
+CREATE UNIQUE INDEX "Account_providerId_accountId_key" ON "Account" USING btree ("providerId","accountId");--> statement-breakpoint
+CREATE UNIQUE INDEX "Session_token_key" ON "Session" USING btree ("token");--> statement-breakpoint
+CREATE UNIQUE INDEX "TeamMember_teamId_userId_key" ON "TeamMember" USING btree ("teamId","userId");--> statement-breakpoint
+CREATE INDEX "TeamMember_teamId_idx" ON "TeamMember" USING btree ("teamId");--> statement-breakpoint
+CREATE INDEX "TeamMember_userId_idx" ON "TeamMember" USING btree ("userId");--> statement-breakpoint
+CREATE INDEX "Team_slug_idx" ON "Team" USING btree ("slug");--> statement-breakpoint
+CREATE INDEX "Verification_identifier_value_idx" ON "Verification" USING btree ("identifier","value");
\ No newline at end of file
diff --git a/templates/packages/db/drizzle/meta/0000_snapshot.json b/templates/packages/db/drizzle/meta/0000_snapshot.json
new file mode 100644
index 00000000..5860008a
--- /dev/null
+++ b/templates/packages/db/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,655 @@
+{
+ "id": "7a1129e4-a4cd-47a2-b232-bb8718867c6f",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.Account": {
+ "name": "Account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "accountId": {
+ "name": "accountId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerId": {
+ "name": "providerId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "accessToken": {
+ "name": "accessToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refreshToken": {
+ "name": "refreshToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "accessTokenExpiresAt": {
+ "name": "accessTokenExpiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refreshTokenExpiresAt": {
+ "name": "refreshTokenExpiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idToken": {
+ "name": "idToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "Account_userId_idx": {
+ "name": "Account_userId_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "Account_providerId_accountId_key": {
+ "name": "Account_providerId_accountId_key",
+ "columns": [
+ {
+ "expression": "providerId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "accountId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Account_userId_User_id_fk": {
+ "name": "Account_userId_User_id_fk",
+ "tableFrom": "Account",
+ "tableTo": "User",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Session": {
+ "name": "Session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ipAddress": {
+ "name": "ipAddress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "userAgent": {
+ "name": "userAgent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "impersonatedBy": {
+ "name": "impersonatedBy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "Session_token_key": {
+ "name": "Session_token_key",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "Session_userId_User_id_fk": {
+ "name": "Session_userId_User_id_fk",
+ "tableFrom": "Session",
+ "tableTo": "User",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Session_token_unique": {
+ "name": "Session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.TeamMember": {
+ "name": "TeamMember",
+ "schema": "",
+ "columns": {
+ "teamId": {
+ "name": "teamId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "team_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "joinedAt": {
+ "name": "joinedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "TeamMember_teamId_userId_key": {
+ "name": "TeamMember_teamId_userId_key",
+ "columns": [
+ {
+ "expression": "teamId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "TeamMember_teamId_idx": {
+ "name": "TeamMember_teamId_idx",
+ "columns": [
+ {
+ "expression": "teamId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "TeamMember_userId_idx": {
+ "name": "TeamMember_userId_idx",
+ "columns": [
+ {
+ "expression": "userId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "TeamMember_teamId_Team_id_fk": {
+ "name": "TeamMember_teamId_Team_id_fk",
+ "tableFrom": "TeamMember",
+ "tableTo": "Team",
+ "columnsFrom": [
+ "teamId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "TeamMember_userId_User_id_fk": {
+ "name": "TeamMember_userId_User_id_fk",
+ "tableFrom": "TeamMember",
+ "tableTo": "User",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Team": {
+ "name": "Team",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "Team_slug_idx": {
+ "name": "Team_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "Team_slug_unique": {
+ "name": "Team_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.User": {
+ "name": "User",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "firstName": {
+ "name": "firstName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lastName": {
+ "name": "lastName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "lastSeenAt": {
+ "name": "lastSeenAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "User_email_unique": {
+ "name": "User_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ },
+ "User_phone_unique": {
+ "name": "User_phone_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "phone"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.Verification": {
+ "name": "Verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "Verification_identifier_value_idx": {
+ "name": "Verification_identifier_value_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "value",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.team_role": {
+ "name": "team_role",
+ "schema": "public",
+ "values": [
+ "owner",
+ "member"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/templates/packages/db/drizzle/meta/_journal.json b/templates/packages/db/drizzle/meta/_journal.json
new file mode 100644
index 00000000..d1b77103
--- /dev/null
+++ b/templates/packages/db/drizzle/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1762370105573,
+ "tag": "0000_initial",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/templates/repo/packages/db/package.json b/templates/packages/db/package.json
similarity index 99%
rename from templates/repo/packages/db/package.json
rename to templates/packages/db/package.json
index fb699bb5..1902b86b 100644
--- a/templates/repo/packages/db/package.json
+++ b/templates/packages/db/package.json
@@ -29,4 +29,4 @@
"@types/pg": "^8.11.10",
"drizzle-kit": "^0.30.2"
}
-}
+}
\ No newline at end of file
diff --git a/templates/packages/db/src/index.ts b/templates/packages/db/src/index.ts
new file mode 100644
index 00000000..96233846
--- /dev/null
+++ b/templates/packages/db/src/index.ts
@@ -0,0 +1,36 @@
+import { drizzle } from "drizzle-orm/node-postgres"
+import { Pool } from "pg"
+import * as schema from "./schema"
+
+if (!process.env.DATABASE_URL) {
+ throw new Error(
+ "DATABASE_URL environment variable is not set. Please add it to your .env.local file."
+ )
+}
+
+const globalForDb = globalThis as unknown as {
+ pool: Pool | undefined
+}
+
+const pool =
+ globalForDb.pool ??
+ new Pool({
+ connectionString: process.env.DATABASE_URL,
+ max: 20,
+ idleTimeoutMillis: 30000,
+ connectionTimeoutMillis: 2000,
+ allowExitOnIdle: true
+ })
+
+// Only attach error listener when creating a new pool, not when reusing cached one
+if (!globalForDb.pool) {
+ pool.on("error", (err) => {
+ console.error("Unexpected database pool error", err)
+ })
+}
+
+if (process.env.NODE_ENV !== "production") globalForDb.pool = pool
+
+export const db = drizzle({ client: pool, schema })
+
+export * from "./schema"
diff --git a/templates/repo/packages/db/src/schema.ts b/templates/packages/db/src/schema.ts
similarity index 98%
rename from templates/repo/packages/db/src/schema.ts
rename to templates/packages/db/src/schema.ts
index e9bb4be6..64803aab 100644
--- a/templates/repo/packages/db/src/schema.ts
+++ b/templates/packages/db/src/schema.ts
@@ -50,7 +50,7 @@ export const accounts = pgTable(
},
(table) => [
index("Account_userId_idx").on(table.userId),
- index("Account_providerId_accountId_idx").on(
+ uniqueIndex("Account_providerId_accountId_key").on(
table.providerId,
table.accountId
)
diff --git a/templates/repo/packages/db/tsconfig.json b/templates/packages/db/tsconfig.json
similarity index 100%
rename from templates/repo/packages/db/tsconfig.json
rename to templates/packages/db/tsconfig.json
diff --git a/templates/repo/packages/emails/.gitignore b/templates/packages/emails/.gitignore
similarity index 100%
rename from templates/repo/packages/emails/.gitignore
rename to templates/packages/emails/.gitignore
diff --git a/templates/repo/packages/emails/package.json b/templates/packages/emails/package.json
similarity index 100%
rename from templates/repo/packages/emails/package.json
rename to templates/packages/emails/package.json
diff --git a/templates/repo/packages/emails/src/index.tsx b/templates/packages/emails/src/index.tsx
similarity index 100%
rename from templates/repo/packages/emails/src/index.tsx
rename to templates/packages/emails/src/index.tsx
diff --git a/templates/repo/packages/emails/src/templates/team-invite.tsx b/templates/packages/emails/src/templates/team-invite.tsx
similarity index 100%
rename from templates/repo/packages/emails/src/templates/team-invite.tsx
rename to templates/packages/emails/src/templates/team-invite.tsx
diff --git a/templates/repo/packages/emails/src/templates/verifiy-code.tsx b/templates/packages/emails/src/templates/verifiy-code.tsx
similarity index 100%
rename from templates/repo/packages/emails/src/templates/verifiy-code.tsx
rename to templates/packages/emails/src/templates/verifiy-code.tsx
diff --git a/templates/repo/packages/emails/tsconfig.json b/templates/packages/emails/tsconfig.json
similarity index 100%
rename from templates/repo/packages/emails/tsconfig.json
rename to templates/packages/emails/tsconfig.json
diff --git a/templates/repo/packages/ui/.gitignore b/templates/packages/ui/.gitignore
similarity index 100%
rename from templates/repo/packages/ui/.gitignore
rename to templates/packages/ui/.gitignore
diff --git a/templates/repo/packages/ui/.storybook/main.ts b/templates/packages/ui/.storybook/main.ts
similarity index 100%
rename from templates/repo/packages/ui/.storybook/main.ts
rename to templates/packages/ui/.storybook/main.ts
diff --git a/templates/repo/packages/ui/.storybook/preview-head.html b/templates/packages/ui/.storybook/preview-head.html
similarity index 100%
rename from templates/repo/packages/ui/.storybook/preview-head.html
rename to templates/packages/ui/.storybook/preview-head.html
diff --git a/templates/repo/packages/ui/.storybook/preview.ts b/templates/packages/ui/.storybook/preview.ts
similarity index 100%
rename from templates/repo/packages/ui/.storybook/preview.ts
rename to templates/packages/ui/.storybook/preview.ts
diff --git a/templates/repo/packages/ui/.storybook/styles.css b/templates/packages/ui/.storybook/styles.css
similarity index 100%
rename from templates/repo/packages/ui/.storybook/styles.css
rename to templates/packages/ui/.storybook/styles.css
diff --git a/templates/repo/packages/ui/biome.jsonc b/templates/packages/ui/biome.jsonc
similarity index 100%
rename from templates/repo/packages/ui/biome.jsonc
rename to templates/packages/ui/biome.jsonc
diff --git a/templates/repo/packages/ui/components.json b/templates/packages/ui/components.json
similarity index 100%
rename from templates/repo/packages/ui/components.json
rename to templates/packages/ui/components.json
diff --git a/templates/repo/packages/ui/package.json b/templates/packages/ui/package.json
similarity index 94%
rename from templates/repo/packages/ui/package.json
rename to templates/packages/ui/package.json
index f9b0c91e..d04c4a12 100644
--- a/templates/repo/packages/ui/package.json
+++ b/templates/packages/ui/package.json
@@ -12,12 +12,9 @@
},
"scripts": {
"clean": "rm -rf .turbo",
- "build": "tailwindcss -i ./src/styles/index.css -o ./dist/styles.css",
- "olddev": "tailwindcss -i ./src/styles/index.css -o ./dist/styles.css --watch",
"lint": "biome lint --unsafe",
"lint:fix": "biome lint --write --unsafe",
"typecheck": "tsc --noEmit",
- "postinstall": "pnpm build",
"storybook": "storybook dev -p 6006"
},
"keywords": [],
diff --git a/templates/repo/packages/ui/postcss.config.mjs b/templates/packages/ui/postcss.config.mjs
similarity index 100%
rename from templates/repo/packages/ui/postcss.config.mjs
rename to templates/packages/ui/postcss.config.mjs
diff --git a/templates/repo/packages/ui/shadcn.sh b/templates/packages/ui/shadcn.sh
similarity index 100%
rename from templates/repo/packages/ui/shadcn.sh
rename to templates/packages/ui/shadcn.sh
diff --git a/templates/packages/ui/src/components/alert-dialog.stories.tsx b/templates/packages/ui/src/components/alert-dialog.stories.tsx
new file mode 100644
index 00000000..e1bb001a
--- /dev/null
+++ b/templates/packages/ui/src/components/alert-dialog.stories.tsx
@@ -0,0 +1,45 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from './alert-dialog';
+import { Button } from './button';
+
+const meta: Meta = {
+ title: 'UI/AlertDialog',
+ component: AlertDialog,
+ tags: ['autodocs'],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Destructive: Story = {
+ render: () => (
+
+
+ Delete Files
+
+
+
+ Delete Selected Files
+
+ Are you sure you want to delete these files? This action cannot be
+ undone.
+
+
+
+ Keep Files
+ Yes, Delete
+
+
+
+ ),
+};
diff --git a/templates/packages/ui/src/components/alert-dialog.tsx b/templates/packages/ui/src/components/alert-dialog.tsx
new file mode 100644
index 00000000..576f3f0a
--- /dev/null
+++ b/templates/packages/ui/src/components/alert-dialog.tsx
@@ -0,0 +1,137 @@
+'use client';
+
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+import { buttonVariants } from './button';
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = 'AlertDialogHeader';
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = 'AlertDialogFooter';
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+};
diff --git a/templates/repo/packages/ui/src/components/alert.stories.tsx b/templates/packages/ui/src/components/alert.stories.tsx
similarity index 74%
rename from templates/repo/packages/ui/src/components/alert.stories.tsx
rename to templates/packages/ui/src/components/alert.stories.tsx
index 11297f5c..73c960bc 100644
--- a/templates/repo/packages/ui/src/components/alert.stories.tsx
+++ b/templates/packages/ui/src/components/alert.stories.tsx
@@ -1,12 +1,15 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
-import { Alert, AlertDescription, AlertTitle } from "./alert";
-import { ExclamationTriangleIcon, InfoCircledIcon, CheckCircledIcon } from "@radix-ui/react-icons";
+import {
+ CheckCircledIcon,
+ ExclamationTriangleIcon,
+ InfoCircledIcon,
+} from '@radix-ui/react-icons';
+import type { Meta, StoryObj } from '@storybook/react';
+import { Alert, AlertDescription, AlertTitle } from './alert';
const meta: Meta = {
- title: "UI/Alert",
+ title: 'UI/Alert',
component: Alert,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
@@ -52,9 +55,7 @@ export const WithoutTitle: Story = {
render: () => (
-
- This is an alert without a title.
-
+ This is an alert without a title.
),
};
diff --git a/templates/packages/ui/src/components/alert.tsx b/templates/packages/ui/src/components/alert.tsx
new file mode 100644
index 00000000..ce00d5b0
--- /dev/null
+++ b/templates/packages/ui/src/components/alert.tsx
@@ -0,0 +1,61 @@
+import { type VariantProps, cva } from 'class-variance-authority';
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+
+const alertVariants = cva(
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
+ {
+ variants: {
+ variant: {
+ default: 'bg-background text-foreground',
+ destructive:
+ 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
+ success:
+ 'border-success/50 text-success dark:border-success [&>svg]:text-success',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = 'Alert';
+
+const AlertTitle = React.forwardRef<
+ HTMLHeadingElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = 'AlertTitle';
+
+const AlertDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = 'AlertDescription';
+
+export { Alert, AlertDescription, AlertTitle };
diff --git a/templates/repo/packages/ui/src/components/avatar.stories.tsx b/templates/packages/ui/src/components/avatar.stories.tsx
similarity index 83%
rename from templates/repo/packages/ui/src/components/avatar.stories.tsx
rename to templates/packages/ui/src/components/avatar.stories.tsx
index d36825a7..6d38f1bb 100644
--- a/templates/repo/packages/ui/src/components/avatar.stories.tsx
+++ b/templates/packages/ui/src/components/avatar.stories.tsx
@@ -1,11 +1,10 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
-import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
+import type { Meta, StoryObj } from '@storybook/react';
+import { Avatar, AvatarFallback, AvatarImage } from './avatar';
const meta: Meta = {
- title: "UI/Avatar",
+ title: 'UI/Avatar',
component: Avatar,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
diff --git a/templates/packages/ui/src/components/avatar.tsx b/templates/packages/ui/src/components/avatar.tsx
new file mode 100644
index 00000000..71cab327
--- /dev/null
+++ b/templates/packages/ui/src/components/avatar.tsx
@@ -0,0 +1,86 @@
+'use client';
+
+import * as AvatarPrimitive from '@radix-ui/react-avatar';
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+
+const COLORS = [
+ 'bg-red-700 text-white',
+ 'bg-blue-700 text-white',
+ 'bg-green-700 text-white',
+ 'bg-yellow-200 text-black',
+ 'bg-purple-600 text-white',
+ 'bg-pink-200 text-black',
+ 'bg-indigo-200 text-black',
+ 'bg-orange-200 text-black',
+] as const;
+
+function getAvatarColor(label: string | null): string {
+ if (!label) return 'bg-gray-200';
+
+ const hash = label.split('').reduce((acc, char) => {
+ return (acc * 31 + char.charCodeAt(0)) | 0;
+ }, 0);
+
+ return COLORS[Math.abs(hash) % COLORS.length] as string;
+}
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+interface AvatarFallbackProps
+ extends React.ComponentPropsWithoutRef {
+ label?: string | null;
+}
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ AvatarFallbackProps
+>(({ className, label, children, ...props }, ref) => (
+
+ {label
+ ? label
+ .trim().split(/\s+/)
+ .map((word) => Array.from(word)[0] ?? '')
+ .slice(0, 2)
+ .join('')
+ .toUpperCase()
+ : children}
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarFallback, AvatarImage };
diff --git a/templates/repo/packages/ui/src/components/badge.stories.tsx b/templates/packages/ui/src/components/badge.stories.tsx
similarity index 50%
rename from templates/repo/packages/ui/src/components/badge.stories.tsx
rename to templates/packages/ui/src/components/badge.stories.tsx
index 2e3fe70b..58b6b6cb 100644
--- a/templates/repo/packages/ui/src/components/badge.stories.tsx
+++ b/templates/packages/ui/src/components/badge.stories.tsx
@@ -1,10 +1,10 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import { Badge } from "./badge";
+import type { Meta, StoryObj } from '@storybook/react';
+import { Badge } from './badge';
const meta: Meta = {
- title: "UI/Badge",
+ title: 'UI/Badge',
component: Badge,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
@@ -12,28 +12,28 @@ type Story = StoryObj;
export const Default: Story = {
args: {
- children: "Badge",
- variant: "default",
+ children: 'Badge',
+ variant: 'default',
},
};
export const Secondary: Story = {
args: {
- children: "Badge",
- variant: "secondary",
+ children: 'Badge',
+ variant: 'secondary',
},
};
export const Destructive: Story = {
args: {
- children: "Badge",
- variant: "destructive",
+ children: 'Badge',
+ variant: 'destructive',
},
};
export const Outline: Story = {
args: {
- children: "Badge",
- variant: "outline",
+ children: 'Badge',
+ variant: 'outline',
},
};
diff --git a/templates/packages/ui/src/components/badge.tsx b/templates/packages/ui/src/components/badge.tsx
new file mode 100644
index 00000000..30ebc470
--- /dev/null
+++ b/templates/packages/ui/src/components/badge.tsx
@@ -0,0 +1,45 @@
+import { type VariantProps, cva } from 'class-variance-authority';
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ success:
+ 'border-transparent bg-green-100 text-green-700 hover:bg-green-100',
+ destructive:
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ outline: 'text-foreground',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+const Badge = React.forwardRef(
+ ({ className, variant, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Badge.displayName = 'Badge';
+
+export { Badge, badgeVariants };
diff --git a/templates/repo/packages/ui/src/components/breadcrumb.stories.tsx b/templates/packages/ui/src/components/breadcrumb.stories.tsx
similarity index 85%
rename from templates/repo/packages/ui/src/components/breadcrumb.stories.tsx
rename to templates/packages/ui/src/components/breadcrumb.stories.tsx
index f779e114..02cfba04 100644
--- a/templates/repo/packages/ui/src/components/breadcrumb.stories.tsx
+++ b/templates/packages/ui/src/components/breadcrumb.stories.tsx
@@ -1,19 +1,18 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
+import type { Meta, StoryObj } from '@storybook/react';
import {
Breadcrumb,
+ BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
- BreadcrumbEllipsis,
-} from "./breadcrumb";
+} from './breadcrumb';
const meta: Meta = {
- title: "UI/Breadcrumb",
+ title: 'UI/Breadcrumb',
component: Breadcrumb,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
@@ -52,7 +51,9 @@ export const WithEllipsis: Story = {
- Breadcrumb
+
+ Breadcrumb
+
diff --git a/templates/packages/ui/src/components/breadcrumb.tsx b/templates/packages/ui/src/components/breadcrumb.tsx
new file mode 100644
index 00000000..71f2d00d
--- /dev/null
+++ b/templates/packages/ui/src/components/breadcrumb.tsx
@@ -0,0 +1,113 @@
+import { Slot } from '@radix-ui/react-slot';
+import { ChevronRight, MoreHorizontal } from 'lucide-react';
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<'nav'> & {
+ separator?: React.ReactNode;
+ }
+>(({ ...props }, ref) => );
+Breadcrumb.displayName = 'Breadcrumb';
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<'ol'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbList.displayName = 'BreadcrumbList';
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<'li'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbItem.displayName = 'BreadcrumbItem';
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<'a'> & {
+ asChild?: boolean;
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'a';
+
+ return (
+
+ );
+});
+BreadcrumbLink.displayName = 'BreadcrumbLink';
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<'span'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbPage.displayName = 'BreadcrumbPage';
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) => (
+ svg]:w-3.5 [&>svg]:h-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+);
+BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) => (
+
+
+ More
+
+);
+BreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis';
+
+export {
+ Breadcrumb,
+ BreadcrumbEllipsis,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+};
diff --git a/templates/packages/ui/src/components/button.stories.tsx b/templates/packages/ui/src/components/button.stories.tsx
new file mode 100644
index 00000000..6f2afb92
--- /dev/null
+++ b/templates/packages/ui/src/components/button.stories.tsx
@@ -0,0 +1,53 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Button } from './button';
+
+const meta: Meta = {
+ title: 'UI/Button',
+ component: Button,
+ tags: ['autodocs'],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {
+ children: 'Button',
+ variant: 'default',
+ },
+};
+
+export const Secondary: Story = {
+ args: {
+ children: 'Button',
+ variant: 'secondary',
+ },
+};
+
+export const Destructive: Story = {
+ args: {
+ children: 'Button',
+ variant: 'destructive',
+ },
+};
+
+export const Outline: Story = {
+ args: {
+ children: 'Button',
+ variant: 'outline',
+ },
+};
+
+export const Ghost: Story = {
+ args: {
+ children: 'Button',
+ variant: 'ghost',
+ },
+};
+
+export const Link: Story = {
+ args: {
+ children: 'Button',
+ variant: 'link',
+ },
+};
diff --git a/templates/packages/ui/src/components/button.tsx b/templates/packages/ui/src/components/button.tsx
new file mode 100644
index 00000000..29f45ea0
--- /dev/null
+++ b/templates/packages/ui/src/components/button.tsx
@@ -0,0 +1,145 @@
+import { Slot } from '@radix-ui/react-slot';
+import { type VariantProps, cva } from 'class-variance-authority';
+import Link from 'next/link';
+import * as React from 'react';
+import { cn } from '../lib/utils';
+
+const buttonVariants = cva(
+ cn(
+ 'inline-flex items-center gap-1 justify-center whitespace-nowrap font-medium ring-offset-background transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
+ ),
+ {
+ variants: {
+ variant: {
+ default:
+ 'bg-black text-white dark:bg-white dark:text-black hover:bg-foreground/90 border border-black dark:border-white',
+ success: 'bg-green-500 text-white hover:bg-green-600',
+ destructive:
+ 'bg-destructive dark:bg-destructive text-destructive-foreground hover:bg-destructive/90 dark:hover:bg-destructive/90',
+ primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ 'primary-outline':
+ 'border-2 text-primary border-primary bg-transparent hover:bg-accent',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ outline:
+ 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-11 rounded-md px-4 py-2',
+ xs: 'h-6 rounded-sm px-1.5',
+ sm: 'h-9 rounded-md px-3',
+ lg: 'h-12 rounded-md text-lg px-6',
+ xl: 'h-16 rounded-md text-xl px-8',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+ loading?: boolean;
+ icon?: React.ReactNode;
+ iconPosition?: 'start' | 'end';
+}
+
+const Button = React.forwardRef(
+ (
+ {
+ className,
+ variant,
+ size,
+ asChild = false,
+ loading = false,
+ icon,
+ iconPosition = 'start',
+ children,
+ disabled,
+ ...props
+ },
+ ref,
+ ) => {
+ const Comp = asChild ? Slot : 'button';
+ return (
+
+ {loading ? (
+ <>
+
+ Loading...
+
+
+
+ {children}
+ >
+ ) : (
+ <>
+ {iconPosition === 'start' && icon}
+ {children}
+ {iconPosition === 'end' && icon}
+ >
+ )}
+
+ );
+ },
+);
+Button.displayName = 'Button';
+
+interface ButtonLinkProps
+ extends React.AnchorHTMLAttributes {
+ href: string;
+ variant?: VariantProps['variant'];
+ size?: VariantProps['size'];
+ icon?: React.ReactNode;
+}
+
+const ButtonLink = React.forwardRef(
+ ({ className, href, variant, size, icon, children, ...props }, ref) => {
+ return (
+
+ {icon}
+ {children}
+
+ );
+ },
+);
+
+ButtonLink.displayName = 'ButtonLink';
+
+export { Button, ButtonLink, buttonVariants };
diff --git a/templates/repo/packages/ui/src/components/card.stories.tsx b/templates/packages/ui/src/components/card.stories.tsx
similarity index 91%
rename from templates/repo/packages/ui/src/components/card.stories.tsx
rename to templates/packages/ui/src/components/card.stories.tsx
index 704b58c9..d0eacb58 100644
--- a/templates/repo/packages/ui/src/components/card.stories.tsx
+++ b/templates/packages/ui/src/components/card.stories.tsx
@@ -1,6 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
-import { Button } from "./button";
+import { Button } from './button';
import {
Card,
CardContent,
@@ -8,12 +7,12 @@ import {
CardFooter,
CardHeader,
CardTitle,
-} from "./card";
+} from './card';
const meta: Meta = {
- title: "UI/Card",
+ title: 'UI/Card',
component: Card,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
diff --git a/templates/packages/ui/src/components/card.tsx b/templates/packages/ui/src/components/card.tsx
new file mode 100644
index 00000000..7e69aa38
--- /dev/null
+++ b/templates/packages/ui/src/components/card.tsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = 'CardFooter';
+
+export {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+};
diff --git a/templates/repo/packages/ui/src/components/checkbox.stories.tsx b/templates/packages/ui/src/components/checkbox.stories.tsx
similarity index 87%
rename from templates/repo/packages/ui/src/components/checkbox.stories.tsx
rename to templates/packages/ui/src/components/checkbox.stories.tsx
index ad011c9d..c69b5e58 100644
--- a/templates/repo/packages/ui/src/components/checkbox.stories.tsx
+++ b/templates/packages/ui/src/components/checkbox.stories.tsx
@@ -1,11 +1,10 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
-import { Checkbox } from "./checkbox";
+import type { Meta, StoryObj } from '@storybook/react';
+import { Checkbox } from './checkbox';
const meta: Meta = {
- title: "UI/Checkbox",
+ title: 'UI/Checkbox',
component: Checkbox,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
diff --git a/templates/packages/ui/src/components/checkbox.tsx b/templates/packages/ui/src/components/checkbox.tsx
new file mode 100644
index 00000000..28414ac5
--- /dev/null
+++ b/templates/packages/ui/src/components/checkbox.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import { Check } from 'lucide-react';
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+
+const Checkbox = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export { Checkbox };
diff --git a/templates/repo/packages/ui/src/components/collapsible.stories.tsx b/templates/packages/ui/src/components/collapsible.stories.tsx
similarity index 77%
rename from templates/repo/packages/ui/src/components/collapsible.stories.tsx
rename to templates/packages/ui/src/components/collapsible.stories.tsx
index 9db91946..4b1e39d6 100644
--- a/templates/repo/packages/ui/src/components/collapsible.stories.tsx
+++ b/templates/packages/ui/src/components/collapsible.stories.tsx
@@ -1,13 +1,17 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./collapsible";
-import { Button } from "./button";
-import { ChevronDown, ChevronUp } from "lucide-react";
+import type { Meta, StoryObj } from '@storybook/react';
+import { ChevronDown, ChevronUp } from 'lucide-react';
+import * as React from 'react';
+import { Button } from './button';
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from './collapsible';
const meta: Meta = {
- title: "UI/Collapsible",
+ title: 'UI/Collapsible',
component: Collapsible,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
@@ -24,9 +28,7 @@ export const Default: Story = {
className="w-[350px] space-y-2"
>
-
- Collapsible Demo
-
+
Collapsible Demo
{isOpen ? (
diff --git a/templates/packages/ui/src/components/collapsible.tsx b/templates/packages/ui/src/components/collapsible.tsx
new file mode 100644
index 00000000..8f29e75a
--- /dev/null
+++ b/templates/packages/ui/src/components/collapsible.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
+
+const Collapsible = CollapsiblePrimitive.Root;
+
+const CollapsibleTrigger = CollapsiblePrimitive.Trigger;
+
+const CollapsibleContent = CollapsiblePrimitive.Content;
+
+export { Collapsible, CollapsibleContent, CollapsibleTrigger };
diff --git a/templates/repo/packages/ui/src/components/dialog.stories.tsx b/templates/packages/ui/src/components/dialog.stories.tsx
similarity index 92%
rename from templates/repo/packages/ui/src/components/dialog.stories.tsx
rename to templates/packages/ui/src/components/dialog.stories.tsx
index c97c23de..6c1bb335 100644
--- a/templates/repo/packages/ui/src/components/dialog.stories.tsx
+++ b/templates/packages/ui/src/components/dialog.stories.tsx
@@ -1,6 +1,5 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
-import { Button } from "./button";
+import type { Meta, StoryObj } from '@storybook/react';
+import { Button } from './button';
import {
Dialog,
DialogContent,
@@ -9,12 +8,12 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from "./dialog";
+} from './dialog';
const meta: Meta = {
- title: "UI/Dialog",
+ title: 'UI/Dialog',
component: Dialog,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
diff --git a/templates/packages/ui/src/components/dialog.tsx b/templates/packages/ui/src/components/dialog.tsx
new file mode 100644
index 00000000..328c9ae2
--- /dev/null
+++ b/templates/packages/ui/src/components/dialog.tsx
@@ -0,0 +1,126 @@
+'use client';
+
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import { X } from 'lucide-react';
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+
+const Dialog = DialogPrimitive.Root;
+
+const DialogTrigger = DialogPrimitive.Trigger;
+
+const DialogPortal = DialogPrimitive.Portal;
+
+const DialogClose = DialogPrimitive.Close;
+
+const DialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
+
+const DialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ closeable?: boolean;
+ }
+>(({ className, children, closeable = true, ...props }, ref) => (
+
+
+
+ {children}
+ {closeable ? (
+
+
+ Close
+
+ ) : null}
+
+
+));
+DialogContent.displayName = DialogPrimitive.Content.displayName;
+
+const DialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogHeader.displayName = 'DialogHeader';
+
+const DialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DialogFooter.displayName = 'DialogFooter';
+
+const DialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogTitle.displayName = DialogPrimitive.Title.displayName;
+
+const DialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DialogDescription.displayName = DialogPrimitive.Description.displayName;
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+};
diff --git a/templates/repo/packages/ui/src/components/drawer.stories.tsx b/templates/packages/ui/src/components/drawer.stories.tsx
similarity index 89%
rename from templates/repo/packages/ui/src/components/drawer.stories.tsx
rename to templates/packages/ui/src/components/drawer.stories.tsx
index 2c0a70e7..11922581 100644
--- a/templates/repo/packages/ui/src/components/drawer.stories.tsx
+++ b/templates/packages/ui/src/components/drawer.stories.tsx
@@ -1,6 +1,5 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
-import { Button } from "./button";
+import type { Meta, StoryObj } from '@storybook/react';
+import { Button } from './button';
import {
Drawer,
DrawerClose,
@@ -10,12 +9,12 @@ import {
DrawerHeader,
DrawerTitle,
DrawerTrigger,
-} from "./drawer";
+} from './drawer';
const meta: Meta = {
- title: "UI/Drawer",
+ title: 'UI/Drawer',
component: Drawer,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
@@ -37,8 +36,8 @@ export const Default: Story = {
- This is the main content area of the drawer.
- You can add form elements or any other content here.
+ This is the main content area of the drawer. You can add form
+ elements or any other content here.
diff --git a/templates/packages/ui/src/components/drawer.tsx b/templates/packages/ui/src/components/drawer.tsx
new file mode 100644
index 00000000..c5a55936
--- /dev/null
+++ b/templates/packages/ui/src/components/drawer.tsx
@@ -0,0 +1,118 @@
+'use client';
+
+import * as React from 'react';
+import { Drawer as DrawerPrimitive } from 'vaul';
+
+import { cn } from '../lib/utils';
+
+const Drawer = ({
+ shouldScaleBackground = true,
+ ...props
+}: React.ComponentProps) => (
+
+);
+Drawer.displayName = 'Drawer';
+
+const DrawerTrigger = DrawerPrimitive.Trigger;
+
+const DrawerPortal = DrawerPrimitive.Portal;
+
+const DrawerClose = DrawerPrimitive.Close;
+
+const DrawerOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
+
+const DrawerContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+));
+DrawerContent.displayName = 'DrawerContent';
+
+const DrawerHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DrawerHeader.displayName = 'DrawerHeader';
+
+const DrawerFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+DrawerFooter.displayName = 'DrawerFooter';
+
+const DrawerTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
+
+const DrawerDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
+
+export {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerOverlay,
+ DrawerPortal,
+ DrawerTitle,
+ DrawerTrigger,
+};
diff --git a/templates/repo/packages/ui/src/components/dropdown-menu.stories.tsx b/templates/packages/ui/src/components/dropdown-menu.stories.tsx
similarity index 93%
rename from templates/repo/packages/ui/src/components/dropdown-menu.stories.tsx
rename to templates/packages/ui/src/components/dropdown-menu.stories.tsx
index 171058bf..ebf81dc4 100644
--- a/templates/repo/packages/ui/src/components/dropdown-menu.stories.tsx
+++ b/templates/packages/ui/src/components/dropdown-menu.stories.tsx
@@ -1,5 +1,7 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
+import type { Meta, StoryObj } from '@storybook/react';
+import { CreditCard, LogOut, Settings, User } from 'lucide-react';
+import * as React from 'react';
+import { Button } from './button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -15,14 +17,12 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
-} from "./dropdown-menu";
-import { Button } from "./button";
-import { User, Settings, CreditCard, LogOut } from "lucide-react";
+} from './dropdown-menu';
const meta: Meta = {
- title: "UI/DropdownMenu",
+ title: 'UI/DropdownMenu',
component: DropdownMenu,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
@@ -105,7 +105,7 @@ export const WithCheckboxItems: Story = {
export const WithRadioItems: Story = {
render: () => {
- const [position, setPosition] = React.useState("bottom");
+ const [position, setPosition] = React.useState('bottom');
return (
diff --git a/templates/packages/ui/src/components/dropdown-menu.tsx b/templates/packages/ui/src/components/dropdown-menu.tsx
new file mode 100644
index 00000000..cf4fdce8
--- /dev/null
+++ b/templates/packages/ui/src/components/dropdown-menu.tsx
@@ -0,0 +1,202 @@
+'use client';
+
+import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
+import { Check, ChevronRight, Circle } from 'lucide-react';
+import * as React from 'react';
+
+import { cn } from '../lib/utils';
+
+const DropdownMenu = DropdownMenuPrimitive.Root;
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName;
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName;
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+));
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+));
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName;
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
+
+export {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+};
diff --git a/templates/repo/packages/ui/src/components/form.stories.tsx b/templates/packages/ui/src/components/form.stories.tsx
similarity index 80%
rename from templates/repo/packages/ui/src/components/form.stories.tsx
rename to templates/packages/ui/src/components/form.stories.tsx
index 77966744..bb1d2042 100644
--- a/templates/repo/packages/ui/src/components/form.stories.tsx
+++ b/templates/packages/ui/src/components/form.stories.tsx
@@ -1,10 +1,9 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import * as React from "react";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import * as z from "zod";
-import { Button } from "./button";
-import { Input } from "./input";
+import { zodResolver } from '@hookform/resolvers/zod';
+import type { Meta, StoryObj } from '@storybook/react';
+import * as React from 'react';
+import { useForm } from 'react-hook-form';
+import * as z from 'zod';
+import { Button } from './button';
import {
Form,
FormControl,
@@ -13,12 +12,13 @@ import {
FormItem,
FormLabel,
FormMessage,
-} from "./form";
+} from './form';
+import { Input } from './input';
const meta: Meta = {
- title: "UI/Form",
+ title: 'UI/Form',
component: Form,
- tags: ["autodocs"],
+ tags: ['autodocs'],
};
export default meta;
@@ -26,10 +26,10 @@ type Story = StoryObj;
const formSchema = z.object({
username: z.string().min(2, {
- message: "Username must be at least 2 characters.",
+ message: 'Username must be at least 2 characters.',
}),
email: z.string().email({
- message: "Please enter a valid email address.",
+ message: 'Please enter a valid email address.',
}),
});
@@ -37,8 +37,8 @@ const BasicFormExample = () => {
const form = useForm>({
resolver: zodResolver(formSchema),
defaultValues: {
- username: "",
- email: "",
+ username: '',
+ email: '',
},
});
@@ -48,7 +48,10 @@ const BasicFormExample = () => {
return (