diff --git a/AGENTS.md b/AGENTS.md index 90d1028dd..cb23a9ac9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,17 +4,15 @@ This Agents.md file provides comprehensive guidance for OpenAI Codex and other A ## Project Overview -Makerkit - Supabase SaaS Starter Kit (Turbo Edition) is a multi-tenant SaaS application built with Next.js, Supabase, and Tailwind CSS. The project uses a Turborepo monorepo structure with distinct apps for the main web application, development tools, and e2e testing. - -## Architecture +Makerkit is a multi-tenant SaaS application using a Turborepo monorepo structure with distinct apps for the main web application, development tools, and e2e testing. ### Monorepo Structure - `/apps/web` - Main Next.js SaaS application -- `/apps/dev-tool` - Development utilities (runs on port 3010) +- `/apps/dev-tool` - Development utilities (port 3010) - `/apps/e2e` - Playwright end-to-end tests - `/packages/` - Shared packages and utilities -- `/tooling/` - Build tools, linting, and development scripts +- `/tooling/` - Build tools and development scripts ### Core Technologies @@ -27,89 +25,101 @@ Makerkit - Supabase SaaS Starter Kit (Turbo Edition) is a multi-tenant SaaS appl ### Multi-Tenant Architecture -The application uses a dual account model: +Uses a dual account model: -- **Personal Accounts**: Individual user accounts (auth.users.id = accounts.id) +- **Personal Accounts**: Individual user accounts (`auth.users.id = accounts.id`) - **Team Accounts**: Shared workspaces with members, roles, and permissions -- Data is associated with accounts via foreign keys for proper access control +- Data associates with accounts via foreign keys for proper access control -## Common Commands +## Essential Commands ### Development ```bash -# Start all apps in development -pnpm dev - -# Start specific app -pnpm --filter web dev # Main app (port 3000) -pnpm --filter dev-tool dev # Dev tools (port 3010) - -# Build all apps -pnpm build - -# Run tests -pnpm test # All tests -pnpm --filter e2e test # E2E tests only -``` - -### Code Quality - -```bash -# Lint all code -pnpm lint -pnpm lint:fix # Auto-fix issues - -# Format code -pnpm format -pnpm format:fix # Auto-fix formatting - -# Type checking -pnpm typecheck +pnpm dev # Start all apps +pnpm --filter web dev # Main app (port 3000) +pnpm --filter dev-tool dev # Dev tools (port 3010) +pnpm build # Build all apps ``` ### Database Operations ```bash -# Start Supabase locally -pnpm supabase:web:start - -# Reset database with latest schema -pnpm supabase:web:reset - -# Generate TypeScript types from database -pnpm supabase:web:typegen - -# Run database tests -pnpm supabase:web:test - -# Create migration from schema changes -pnpm --filter web supabase:db:diff - -# Stop Supabase -pnpm supabase:web:stop +pnpm supabase:web:start # Start Supabase locally +pnpm supabase:web:reset # Reset with latest schema +pnpm supabase:web:typegen # Generate TypeScript types +pnpm --filter web supabase:db:diff # Create migration ``` +### Code Quality + +```bash +pnpm lint && pnpm format # Lint and format +pnpm typecheck # Type checking +pnpm test # Run tests +``` + +## Application Structure + +### Route Organization + +``` +app/ +├── (marketing)/ # Public pages (landing, blog, docs) +├── (auth)/ # Authentication pages +├── home/ +│ ├── (user)/ # Personal account context +│ └── [account]/ # Team account context ([account] = team slug) +├── admin/ # Super admin section +└── api/ # API routes +``` + +See complete structure in @apps/web/app/ with examples like: + +- Marketing layout: @apps/web/app/(marketing)/layout.tsx +- Personal dashboard: @apps/web/app/home/(user)/page.tsx +- Team workspace: @apps/web/app/home/[account]/page.tsx +- Admin section: @apps/web/app/admin/page.tsx + +### Component Organization + +- **Route-specific**: Use `_components/` directories +- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side +- **Global components**: Root-level directories + +Example organization: + +- Team components: @apps/web/app/home/[account]/\_components/ +- Team server utils: @apps/web/app/home/[account]/\_lib/server/ +- Marketing components: @apps/web/app/(marketing)/\_components/ + ## Database Guidelines -### Schema Management - -- Database schemas are in `apps/web/supabase/schemas/` -- Create new schemas as `-.sql` -- After schema changes: run `pnpm --filter web supabase:db:diff` then `pnpm supabase:web:reset` - ### Security & RLS - **Always enable RLS** on new tables unless explicitly instructed otherwise - Use helper functions for access control: - `public.has_role_on_account(account_id, role?)` - Check team membership - - `public.has_permission(user_id, account_id, permission)` - Check specific permissions - - `public.is_account_owner(account_id)` - Verify account ownership -- Associate data with accounts using foreign keys for proper access control + - `public.has_permission(user_id, account_id, permission)` - Check permissions + - `public.is_account_owner(account_id)` - Verify ownership + +See RLS examples in database schemas: @apps/web/supabase/schemas/ + +### Schema Management + +- Schemas in `apps/web/supabase/schemas/` +- Create as `-.sql` +- After changes: `pnpm --filter web supabase:db:diff` then `pnpm supabase:web:reset` + +Key schema files: + +- Accounts: `apps/web/supabase/schemas/03-accounts.sql` +- Memberships: `apps/web/supabase/schemas/05-memberships.sql` +- Permissions: `apps/web/supabase/schemas/06-roles-permissions.sql` ### Type Generation -Import auto-generated types from `@kit/supabase/database`: +Import auto-generated types from @packages/supabase/src/types/database.ts: ```typescript import { Tables } from '@kit/supabase/database'; @@ -121,50 +131,427 @@ type Account = Tables<'accounts'>; ### Data Fetching -- **Server Components**: Use `getSupabaseServerClient()` directly +- **Server Components**: Use `getSupabaseServerClient()` from @packages/supabase/src/clients/server-client.ts - **Client Components**: Use `useSupabase()` hook + React Query's `useQuery` -- Prefer Server Components and pass data down to Client Components when needed +- **Admin Operations**: Use `getSupabaseServerAdminClient()` from @packages/supabase/src/clients/server-admin-client.ts (rare cases only - bypasses RLS, use with caution!) +- Prefer Server Components and pass data down when needed + +Use the Container/Presenter pattern for complex data components: + +```typescript +// Container: handles data fetching +function UserProfileContainer() { + const userData = useUserData(); + return ; +} + +// Presenter: handles UI rendering +function UserProfilePresenter({ data }: { data: UserData }) { + return
{data.name}
; +} +``` + +Example server-side data loading: + +- User workspace loader: @apps/web/app/home/(user)/\_lib/server/load-user-workspace.ts +- Team workspace loader: @apps/web/app/home/[account]/\_lib/server/team-account-workspace.loader.ts +- Data provider pattern: @packages/features/team-accounts/src/components/members/roles-data-provider.tsx ### Server Actions -- Always use `enhanceAction` from `@kit/next/actions` -- Name files as `server-actions.ts` and functions with `Action` suffix -- Place Zod schemas in separate files for reuse with forms -- Use `'use server'` directive at top of file +Always use `enhanceAction` from @packages/next/src/actions/index.ts: -### Error Handling & Logging +```typescript +'use server'; -- Use `@kit/shared/logger` for logging -- Don't swallow errors - handle them appropriately -- Provide context without sensitive data +import { enhanceAction } from '@kit/next/actions'; -### Component Organization +export const createNoteAction = enhanceAction( + async function (data, user) { + // data is validated, user is authenticated + return { success: true }; + }, + { + auth: true, + schema: CreateNoteSchema, + }, +); +``` -- Route-specific components in `_components/` directories -- Route-specific utilities in `_lib/` directories -- Server-side utilities in `_lib/server/` -- Global components and utilities in root-level directories +Example server actions: + +- Team billing: @apps/web/app/home/[account]/billing/\_lib/server/server-actions.ts +- Personal settings: @apps/web/app/home/(user)/settings/\_lib/server/server-actions.ts + +### Forms with React Hook Form & Zod + +```typescript +// 1. Define schema in separate file +export const CreateNoteSchema = z.object({ + title: z.string().min(1), + content: z.string().min(1), +}); + +// 2. Client component with form +('use client'); +const form = useForm({ + resolver: zodResolver(CreateNoteSchema), +}); + +const onSubmit = (data) => { + startTransition(async () => { + await toast.promise(createNoteAction(data), { + loading: 'Creating...', + success: 'Created!', + error: 'Failed!', + }); + }); +}; +``` + +See form examples: + +- Contact form: @apps/web/app/(marketing)/contact/\_components/contact-form.tsx +- Verify OTP form: @packages/otp/src/components/verify-otp-form.tsx + +### Route Handlers (API Routes) + +Use `enhanceRouteHandler` from @packages/next/src/routes/index.ts: + +```typescript +import { enhanceRouteHandler } from '@kit/next/routes'; + +export const POST = enhanceRouteHandler( + async function ({ body, user, request }) { + // body is validated, user available if auth: true + return NextResponse.json({ success: true }); + }, + { + auth: true, + schema: ZodSchema, + }, +); +``` + +Example API routes: + +- Billing webhook: @apps/web/app/api/billing/webhook/route.ts +- Database webhook: @apps/web/app/api/db/webhook/route.ts + +## React & TypeScript Best Practices + +### Components + +- Use functional components with TypeScript +- Always use `'use client'` directive for client components +- Destructure props with proper TypeScript interfaces + +### Conditional Rendering + +Use the `If` component from @packages/ui/src/makerkit/if.tsx: + +```tsx +import { If } from '@kit/ui/if'; +import { Spinner } '@kit/ui/spinner'; + +}> + + + +// With type inference + + {(err) => } + +``` + +### Testing Attributes + +Add data attributes for testing: + +```tsx + +
Profile
+
Form content
+``` + +### Internationalization + +Always use `Trans` component from @packages/ui/src/makerkit/trans.tsx: + +```tsx +import { Trans } from '@kit/ui/trans'; + +// Basic usage + + +// With HTML elements +, + }} + defaults="I agree to the Terms." +/> + +// Pluralization + +``` + +Use `LanguageSelector` component from @packages/ui/src/makerkit/language-selector.tsx: + +```tsx +import { LanguageSelector } from '@kit/ui/language-selector'; + +; +``` + +Adding new languages: + +1. Add language code to @apps/web/lib/i18n/i18n.settings.ts +2. Create translation files in @apps/web/public/locales/[new-language]/ +3. Copy structure from English files as template + +Adding new namespaces: + +1. Add namespace to `defaultI18nNamespaces` in @apps/web/lib/i18n/i18n.settings.ts +2. Create corresponding translation files for all supported languages + +Translation files located in @apps/web/public/locales//.json: + +- Common translations: @apps/web/public/locales/en/common.json +- Auth translations: @apps/web/public/locales/en/auth.json +- Team translations: @apps/web/public/locales/en/teams.json + +## Security Guidelines + +### Authentication & Authorization + +- Authentication enforced by middleware +- Authorization typically handled by RLS at database level, unless using the admin client +- For rare admin client usage, enforce both manually +- User authentication helper: @apps/web/lib/server/require-user-in-server-component.ts if required or to obtain the authed user + +### Data Passing + +- **Never pass sensitive data** to Client Components +- **Never expose server environment variables** to client (unless `NEXT_PUBLIC_`) +- Always validate user input before processing + +### OTP for Sensitive Operations + +Use one-time tokens from @packages/otp/src/api/index.ts for destructive operations: + +```tsx +import { VerifyOtpForm } from '@kit/otp/components'; + + { + // Proceed with verified operation + }} +/>; +``` + +OTP schema and functions: @apps/web/supabase/schemas/12-one-time-tokens.sql + +### Super Admin Protection + +For admin routes, use `AdminGuard` from @packages/features/admin/src/components/admin-guard.tsx: + +```tsx +import { AdminGuard } from '@kit/admin/components/admin-guard'; + +export default AdminGuard(AdminPageComponent); +``` + +For admin server actions, use `adminAction` wrapper: + +```tsx +import { adminAction } from '@kit/admin'; + +export const yourAdminAction = adminAction( + enhanceAction( + async (data) => { + // Action implementation + }, + { schema: YourActionSchema }, + ), +); +``` + +Admin service security pattern: + +```typescript +private async assertUserIsNotCurrentSuperAdmin(targetId: string) { + const { data } = await this.client.auth.getUser(); + const currentUserId = data.user?.id; + + if (currentUserId === targetId) { + throw new Error('Cannot perform destructive action on your own account'); + } +} +``` + +## UI Components + +### Core UI Library + +Import from @packages/ui/src/: + +```tsx +// Shadcn components +import { Button } from '@kit/ui/button'; +// @packages/ui/src/shadcn/button.tsx +import { Card } from '@kit/ui/card'; +// @packages/ui/src/shadcn/sonner.tsx + +// Makerkit components +import { If } from '@kit/ui/if'; +// @packages/ui/src/makerkit/trans.tsx +import { ProfileAvatar } from '@kit/ui/profile-avatar'; +// @packages/ui/src/shadcn/card.tsx +import { toast } from '@kit/ui/sonner'; +// @packages/ui/src/makerkit/if.tsx +import { Trans } from '@kit/ui/trans'; + +// @packages/ui/src/makerkit/profile-avatar.tsx +``` + +### Key Component Categories + +- **Forms**: Form components in @packages/ui/src/shadcn/form.tsx +- **Navigation**: Navigation menu in @packages/ui/src/shadcn/navigation-menu.tsx +- **Data Display**: Data table in @packages/ui/src/makerkit/data-table.tsx +- **Marketing**: Marketing components in @packages/ui/src/makerkit/marketing/ + +### Styling + +- Use Tailwind CSS with semantic classes +- Prefer `bg-background`, `text-muted-foreground` over fixed colors +- Use `cn()` utility from @packages/ui/src/lib/utils.ts for class merging + +## Workspace Contexts + +### Personal Account Context (`/home/(user)`) + +Use hook from `packages/features/accounts/src/hooks/use-user-workspace.ts`: + +```tsx +import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace'; + +function PersonalComponent() { + const { user, account } = useUserWorkspace(); + // Personal account data +} +``` + +Context provider: `packages/features/accounts/src/components/user-workspace-context-provider.tsx` + +### Team Account Context (`/home/[account]`) + +Use hook from `packages/features/team-accounts/src/hooks/use-team-account-workspace.ts`: + +```tsx +import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace'; + +function TeamComponent() { + const { account, user, accounts } = useTeamAccountWorkspace(); + // Team account data with permissions +} +``` + +Context provider: `packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx` + +## Error Handling & Logging + +### Structured Logging + +Use logger from `packages/shared/src/logger/logger.ts`: + +```typescript +import { getLogger } from '@kit/shared/logger'; + +const logger = await getLogger(); +const ctx = { name: 'myOperation', userId: user.id }; + +logger.info(ctx, 'Operation started'); +// ... operation +logger.error({ ...ctx, error }, 'Operation failed'); +``` + +### Error Boundaries + +Use proper error handling with meaningful user messages: + +```tsx +try { + await operation(); +} catch (error) { + logger.error({ error, context }, 'Operation failed'); + return { error: 'Unable to complete operation' }; // Generic message +} +``` + +## Feature Development Workflow + +### Creating New Pages + +1. Create page component in appropriate route group +2. Add `withI18n()` HOC from `apps/web/lib/i18n/with-i18n.tsx` +3. Implement `generateMetadata()` for SEO +4. Add loading state with `loading.tsx` +5. Create components in `_components/` directory +6. Add server utilities in `_lib/server/` + +Example page structure: + +- Marketing page: `apps/web/app/(marketing)/pricing/page.tsx` +- Dashboard page: `apps/web/app/home/(user)/page.tsx` +- Team page: `apps/web/app/home/[account]/members/page.tsx` ### Permission Patterns -- Check permissions before data operations using helper functions -- Guard premium features with subscription checks (`public.has_active_subscription`) -- Use role hierarchy to control member management actions -- Primary account owners have special privileges that cannot be revoked +- Check permissions before data operations +- Guard premium features with `public.has_active_subscription` +- Use role hierarchy for member management +- Primary account owners have special privileges -## Testing +Permission helpers in database: `apps/web/supabase/schemas/06-roles-permissions.sql` -### E2E Tests +### Database Development -```bash -pnpm --filter e2e test # Run all E2E tests -``` +1. Create schema file: `apps/web/supabase/schemas/-.sql` +2. Enable RLS and create policies +3. Generate migration: `pnpm --filter web supabase:db:diff` +4. Reset database: `pnpm supabase:web:reset` +5. Generate types: `pnpm supabase:web:typegen` -Test files are in `apps/e2e/tests/` organized by feature area. +## API Services -## Important Notes +### Account Services -- Uses pnpm as package manager -- Database types are auto-generated - don't write manually if shape matches DB -- Always use explicit schema references in SQL (`public.table_name`) -- Documentation available at: https://makerkit.dev/docs/next-supabase-turbo/introduction +- Personal accounts API: `packages/features/accounts/src/server/api.ts` +- Team accounts API: `packages/features/team-accounts/src/server/api.ts` +- Admin service: `packages/features/admin/src/lib/server/services/admin.service.ts` + +### Billing Services + +- Personal billing: `apps/web/app/home/(user)/billing/_lib/server/user-billing.service.ts` +- Team billing: `apps/web/app/home/[account]/billing/_lib/server/team-billing.service.ts` +- Per-seat billing: `packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts` + +## Key Configuration Files + +- **Feature flags**: @apps/web/config/feature-flags.config.ts +- **i18n settings**: @apps/web/lib/i18n/i18n.settings.ts +- **Supabase local config**: @apps/web/supabase/config.toml@ +- **Middleware**: @apps/web/middleware.ts` diff --git a/CLAUDE.md b/CLAUDE.md index 9b3221045..d3c65328a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,17 +4,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Makerkit - Supabase SaaS Starter Kit (Turbo Edition) is a multi-tenant SaaS application built with Next.js, Supabase, and Tailwind CSS. The project uses a Turborepo monorepo structure with distinct apps for the main web application, development tools, and e2e testing. - -## Architecture +Makerkit is a multi-tenant SaaS application using a Turborepo monorepo structure with distinct apps for the main web application, development tools, and e2e testing. ### Monorepo Structure - `/apps/web` - Main Next.js SaaS application -- `/apps/dev-tool` - Development utilities (runs on port 3010) +- `/apps/dev-tool` - Development utilities (port 3010) - `/apps/e2e` - Playwright end-to-end tests - `/packages/` - Shared packages and utilities -- `/tooling/` - Build tools, linting, and development scripts +- `/tooling/` - Build tools and development scripts ### Core Technologies @@ -27,89 +25,101 @@ Makerkit - Supabase SaaS Starter Kit (Turbo Edition) is a multi-tenant SaaS appl ### Multi-Tenant Architecture -The application uses a dual account model: +Uses a dual account model: -- **Personal Accounts**: Individual user accounts (auth.users.id = accounts.id) +- **Personal Accounts**: Individual user accounts (`auth.users.id = accounts.id`) - **Team Accounts**: Shared workspaces with members, roles, and permissions -- Data is associated with accounts via foreign keys for proper access control +- Data associates with accounts via foreign keys for proper access control -## Common Commands +## Essential Commands ### Development ```bash -# Start all apps in development -pnpm dev - -# Start specific app -pnpm --filter web dev # Main app (port 3000) -pnpm --filter dev-tool dev # Dev tools (port 3010) - -# Build all apps -pnpm build - -# Run tests -pnpm test # All tests -pnpm --filter e2e test # E2E tests only -``` - -### Code Quality - -```bash -# Lint all code -pnpm lint -pnpm lint:fix # Auto-fix issues - -# Format code -pnpm format -pnpm format:fix # Auto-fix formatting - -# Type checking -pnpm typecheck +pnpm dev # Start all apps +pnpm --filter web dev # Main app (port 3000) +pnpm --filter dev-tool dev # Dev tools (port 3010) +pnpm build # Build all apps ``` ### Database Operations ```bash -# Start Supabase locally -pnpm supabase:web:start - -# Reset database with latest schema -pnpm supabase:web:reset - -# Generate TypeScript types from database -pnpm supabase:web:typegen - -# Run database tests -pnpm supabase:web:test - -# Create migration from schema changes -pnpm --filter web supabase:db:diff - -# Stop Supabase -pnpm supabase:web:stop +pnpm supabase:web:start # Start Supabase locally +pnpm supabase:web:reset # Reset with latest schema +pnpm supabase:web:typegen # Generate TypeScript types +pnpm --filter web supabase:db:diff # Create migration ``` +### Code Quality + +```bash +pnpm lint && pnpm format # Lint and format +pnpm typecheck # Type checking +pnpm test # Run tests +``` + +## Application Structure + +### Route Organization + +``` +app/ +├── (marketing)/ # Public pages (landing, blog, docs) +├── (auth)/ # Authentication pages +├── home/ +│ ├── (user)/ # Personal account context +│ └── [account]/ # Team account context ([account] = team slug) +├── admin/ # Super admin section +└── api/ # API routes +``` + +See complete structure in @apps/web/app/ with examples like: + +- Marketing layout: @apps/web/app/(marketing)/layout.tsx +- Personal dashboard: @apps/web/app/home/(user)/page.tsx +- Team workspace: @apps/web/app/home/[account]/page.tsx +- Admin section: @apps/web/app/admin/page.tsx + +### Component Organization + +- **Route-specific**: Use `_components/` directories +- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side +- **Global components**: Root-level directories + +Example organization: + +- Team components: @apps/web/app/home/[account]/\_components/ +- Team server utils: @apps/web/app/home/[account]/\_lib/server/ +- Marketing components: @apps/web/app/(marketing)/\_components/ + ## Database Guidelines -### Schema Management - -- Database schemas are in `apps/web/supabase/schemas/` -- Create new schemas as `-.sql` -- After schema changes: run `pnpm --filter web supabase:db:diff` then `pnpm supabase:web:reset` - ### Security & RLS - **Always enable RLS** on new tables unless explicitly instructed otherwise - Use helper functions for access control: - `public.has_role_on_account(account_id, role?)` - Check team membership - - `public.has_permission(user_id, account_id, permission)` - Check specific permissions - - `public.is_account_owner(account_id)` - Verify account ownership -- Associate data with accounts using foreign keys for proper access control + - `public.has_permission(user_id, account_id, permission)` - Check permissions + - `public.is_account_owner(account_id)` - Verify ownership + +See RLS examples in database schemas: @apps/web/supabase/schemas/ + +### Schema Management + +- Schemas in `apps/web/supabase/schemas/` +- Create as `-.sql` +- After changes: `pnpm --filter web supabase:db:diff` then `pnpm supabase:web:reset` + +Key schema files: + +- Accounts: `apps/web/supabase/schemas/03-accounts.sql` +- Memberships: `apps/web/supabase/schemas/05-memberships.sql` +- Permissions: `apps/web/supabase/schemas/06-roles-permissions.sql` ### Type Generation -Import auto-generated types from `@kit/supabase/database`: +Import auto-generated types from @packages/supabase/src/types/database.ts: ```typescript import { Tables } from '@kit/supabase/database'; @@ -121,50 +131,427 @@ type Account = Tables<'accounts'>; ### Data Fetching -- **Server Components**: Use `getSupabaseServerClient()` directly +- **Server Components**: Use `getSupabaseServerClient()` from @packages/supabase/src/clients/server-client.ts - **Client Components**: Use `useSupabase()` hook + React Query's `useQuery` -- Prefer Server Components and pass data down to Client Components when needed +- **Admin Operations**: Use `getSupabaseServerAdminClient()` from @packages/supabase/src/clients/server-admin-client.ts (rare cases only - bypasses RLS, use with caution!) +- Prefer Server Components and pass data down when needed + +Use the Container/Presenter pattern for complex data components: + +```typescript +// Container: handles data fetching +function UserProfileContainer() { + const userData = useUserData(); + return ; +} + +// Presenter: handles UI rendering +function UserProfilePresenter({ data }: { data: UserData }) { + return
{data.name}
; +} +``` + +Example server-side data loading: + +- User workspace loader: @apps/web/app/home/(user)/\_lib/server/load-user-workspace.ts +- Team workspace loader: @apps/web/app/home/[account]/\_lib/server/team-account-workspace.loader.ts +- Data provider pattern: @packages/features/team-accounts/src/components/members/roles-data-provider.tsx ### Server Actions -- Always use `enhanceAction` from `@kit/next/actions` -- Name files as `server-actions.ts` and functions with `Action` suffix -- Place Zod schemas in separate files for reuse with forms -- Use `'use server'` directive at top of file +Always use `enhanceAction` from @packages/next/src/actions/index.ts: -### Error Handling & Logging +```typescript +'use server'; -- Use `@kit/shared/logger` for logging -- Don't swallow errors - handle them appropriately -- Provide context without sensitive data +import { enhanceAction } from '@kit/next/actions'; -### Component Organization +export const createNoteAction = enhanceAction( + async function (data, user) { + // data is validated, user is authenticated + return { success: true }; + }, + { + auth: true, + schema: CreateNoteSchema, + }, +); +``` -- Route-specific components in `_components/` directories -- Route-specific utilities in `_lib/` directories -- Server-side utilities in `_lib/server/` -- Global components and utilities in root-level directories +Example server actions: + +- Team billing: @apps/web/app/home/[account]/billing/\_lib/server/server-actions.ts +- Personal settings: @apps/web/app/home/(user)/settings/\_lib/server/server-actions.ts + +### Forms with React Hook Form & Zod + +```typescript +// 1. Define schema in separate file +export const CreateNoteSchema = z.object({ + title: z.string().min(1), + content: z.string().min(1), +}); + +// 2. Client component with form +('use client'); +const form = useForm({ + resolver: zodResolver(CreateNoteSchema), +}); + +const onSubmit = (data) => { + startTransition(async () => { + await toast.promise(createNoteAction(data), { + loading: 'Creating...', + success: 'Created!', + error: 'Failed!', + }); + }); +}; +``` + +See form examples: + +- Contact form: @apps/web/app/(marketing)/contact/\_components/contact-form.tsx +- Verify OTP form: @packages/otp/src/components/verify-otp-form.tsx + +### Route Handlers (API Routes) + +Use `enhanceRouteHandler` from @packages/next/src/routes/index.ts: + +```typescript +import { enhanceRouteHandler } from '@kit/next/routes'; + +export const POST = enhanceRouteHandler( + async function ({ body, user, request }) { + // body is validated, user available if auth: true + return NextResponse.json({ success: true }); + }, + { + auth: true, + schema: ZodSchema, + }, +); +``` + +Example API routes: + +- Billing webhook: @apps/web/app/api/billing/webhook/route.ts +- Database webhook: @apps/web/app/api/db/webhook/route.ts + +## React & TypeScript Best Practices + +### Components + +- Use functional components with TypeScript +- Always use `'use client'` directive for client components +- Destructure props with proper TypeScript interfaces + +### Conditional Rendering + +Use the `If` component from @packages/ui/src/makerkit/if.tsx: + +```tsx +import { If } from '@kit/ui/if'; +import { Spinner } '@kit/ui/spinner'; + +}> + + + +// With type inference + + {(err) => } + +``` + +### Testing Attributes + +Add data attributes for testing: + +```tsx + +
Profile
+
Form content
+``` + +### Internationalization + +Always use `Trans` component from @packages/ui/src/makerkit/trans.tsx: + +```tsx +import { Trans } from '@kit/ui/trans'; + +// Basic usage + + +// With HTML elements +, + }} + defaults="I agree to the Terms." +/> + +// Pluralization + +``` + +Use `LanguageSelector` component from @packages/ui/src/makerkit/language-selector.tsx: + +```tsx +import { LanguageSelector } from '@kit/ui/language-selector'; + +; +``` + +Adding new languages: + +1. Add language code to @apps/web/lib/i18n/i18n.settings.ts +2. Create translation files in @apps/web/public/locales/[new-language]/ +3. Copy structure from English files as template + +Adding new namespaces: + +1. Add namespace to `defaultI18nNamespaces` in @apps/web/lib/i18n/i18n.settings.ts +2. Create corresponding translation files for all supported languages + +Translation files located in @apps/web/public/locales//.json: + +- Common translations: @apps/web/public/locales/en/common.json +- Auth translations: @apps/web/public/locales/en/auth.json +- Team translations: @apps/web/public/locales/en/teams.json + +## Security Guidelines + +### Authentication & Authorization + +- Authentication enforced by middleware +- Authorization typically handled by RLS at database level, unless using the admin client +- For rare admin client usage, enforce both manually +- User authentication helper: @apps/web/lib/server/require-user-in-server-component.ts if required or to obtain the authed user + +### Data Passing + +- **Never pass sensitive data** to Client Components +- **Never expose server environment variables** to client (unless `NEXT_PUBLIC_`) +- Always validate user input before processing + +### OTP for Sensitive Operations + +Use one-time tokens from @packages/otp/src/api/index.ts for destructive operations: + +```tsx +import { VerifyOtpForm } from '@kit/otp/components'; + + { + // Proceed with verified operation + }} +/>; +``` + +OTP schema and functions: @apps/web/supabase/schemas/12-one-time-tokens.sql + +### Super Admin Protection + +For admin routes, use `AdminGuard` from @packages/features/admin/src/components/admin-guard.tsx: + +```tsx +import { AdminGuard } from '@kit/admin/components/admin-guard'; + +export default AdminGuard(AdminPageComponent); +``` + +For admin server actions, use `adminAction` wrapper: + +```tsx +import { adminAction } from '@kit/admin'; + +export const yourAdminAction = adminAction( + enhanceAction( + async (data) => { + // Action implementation + }, + { schema: YourActionSchema }, + ), +); +``` + +Admin service security pattern: + +```typescript +private async assertUserIsNotCurrentSuperAdmin(targetId: string) { + const { data } = await this.client.auth.getUser(); + const currentUserId = data.user?.id; + + if (currentUserId === targetId) { + throw new Error('Cannot perform destructive action on your own account'); + } +} +``` + +## UI Components + +### Core UI Library + +Import from @packages/ui/src/: + +```tsx +// Shadcn components +import { Button } from '@kit/ui/button'; +// @packages/ui/src/shadcn/button.tsx +import { Card } from '@kit/ui/card'; +// @packages/ui/src/shadcn/sonner.tsx + +// Makerkit components +import { If } from '@kit/ui/if'; +// @packages/ui/src/makerkit/trans.tsx +import { ProfileAvatar } from '@kit/ui/profile-avatar'; +// @packages/ui/src/shadcn/card.tsx +import { toast } from '@kit/ui/sonner'; +// @packages/ui/src/makerkit/if.tsx +import { Trans } from '@kit/ui/trans'; + +// @packages/ui/src/makerkit/profile-avatar.tsx +``` + +### Key Component Categories + +- **Forms**: Form components in @packages/ui/src/shadcn/form.tsx +- **Navigation**: Navigation menu in @packages/ui/src/shadcn/navigation-menu.tsx +- **Data Display**: Data table in @packages/ui/src/makerkit/data-table.tsx +- **Marketing**: Marketing components in @packages/ui/src/makerkit/marketing/ + +### Styling + +- Use Tailwind CSS with semantic classes +- Prefer `bg-background`, `text-muted-foreground` over fixed colors +- Use `cn()` utility from @packages/ui/src/lib/utils.ts for class merging + +## Workspace Contexts + +### Personal Account Context (`/home/(user)`) + +Use hook from `packages/features/accounts/src/hooks/use-user-workspace.ts`: + +```tsx +import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace'; + +function PersonalComponent() { + const { user, account } = useUserWorkspace(); + // Personal account data +} +``` + +Context provider: `packages/features/accounts/src/components/user-workspace-context-provider.tsx` + +### Team Account Context (`/home/[account]`) + +Use hook from `packages/features/team-accounts/src/hooks/use-team-account-workspace.ts`: + +```tsx +import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace'; + +function TeamComponent() { + const { account, user, accounts } = useTeamAccountWorkspace(); + // Team account data with permissions +} +``` + +Context provider: `packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx` + +## Error Handling & Logging + +### Structured Logging + +Use logger from `packages/shared/src/logger/logger.ts`: + +```typescript +import { getLogger } from '@kit/shared/logger'; + +const logger = await getLogger(); +const ctx = { name: 'myOperation', userId: user.id }; + +logger.info(ctx, 'Operation started'); +// ... operation +logger.error({ ...ctx, error }, 'Operation failed'); +``` + +### Error Boundaries + +Use proper error handling with meaningful user messages: + +```tsx +try { + await operation(); +} catch (error) { + logger.error({ error, context }, 'Operation failed'); + return { error: 'Unable to complete operation' }; // Generic message +} +``` + +## Feature Development Workflow + +### Creating New Pages + +1. Create page component in appropriate route group +2. Add `withI18n()` HOC from `apps/web/lib/i18n/with-i18n.tsx` +3. Implement `generateMetadata()` for SEO +4. Add loading state with `loading.tsx` +5. Create components in `_components/` directory +6. Add server utilities in `_lib/server/` + +Example page structure: + +- Marketing page: `apps/web/app/(marketing)/pricing/page.tsx` +- Dashboard page: `apps/web/app/home/(user)/page.tsx` +- Team page: `apps/web/app/home/[account]/members/page.tsx` ### Permission Patterns -- Check permissions before data operations using helper functions -- Guard premium features with subscription checks (`public.has_active_subscription`) -- Use role hierarchy to control member management actions -- Primary account owners have special privileges that cannot be revoked +- Check permissions before data operations +- Guard premium features with `public.has_active_subscription` +- Use role hierarchy for member management +- Primary account owners have special privileges -## Testing +Permission helpers in database: `apps/web/supabase/schemas/06-roles-permissions.sql` -### E2E Tests +### Database Development -```bash -pnpm --filter e2e test # Run all E2E tests -``` +1. Create schema file: `apps/web/supabase/schemas/-.sql` +2. Enable RLS and create policies +3. Generate migration: `pnpm --filter web supabase:db:diff` +4. Reset database: `pnpm supabase:web:reset` +5. Generate types: `pnpm supabase:web:typegen` -Test files are in `apps/e2e/tests/` organized by feature area. +## API Services -## Important Notes +### Account Services -- Uses pnpm as package manager -- Database types are auto-generated - don't write manually if shape matches DB -- Always use explicit schema references in SQL (`public.table_name`) -- Documentation available at: https://makerkit.dev/docs/next-supabase-turbo/introduction +- Personal accounts API: `packages/features/accounts/src/server/api.ts` +- Team accounts API: `packages/features/team-accounts/src/server/api.ts` +- Admin service: `packages/features/admin/src/lib/server/services/admin.service.ts` + +### Billing Services + +- Personal billing: `apps/web/app/home/(user)/billing/_lib/server/user-billing.service.ts` +- Team billing: `apps/web/app/home/[account]/billing/_lib/server/team-billing.service.ts` +- Per-seat billing: `packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts` + +## Key Configuration Files + +- **Feature flags**: @apps/web/config/feature-flags.config.ts +- **i18n settings**: @apps/web/lib/i18n/i18n.settings.ts +- **Supabase local config**: @apps/web/supabase/config.toml@ +- **Middleware**: @apps/web/middleware.ts` \ No newline at end of file diff --git a/apps/dev-tool/app/variables/lib/env-variables-model.ts b/apps/dev-tool/app/variables/lib/env-variables-model.ts index f4a51b2c5..40cdd1c85 100644 --- a/apps/dev-tool/app/variables/lib/env-variables-model.ts +++ b/apps/dev-tool/app/variables/lib/env-variables-model.ts @@ -883,7 +883,8 @@ export const envVariables: EnvVariableModel[] = [ }, { name: 'EMAIL_TLS', - description: 'Whether to use TLS for SMTP connection. Please check this in your SMTP provider settings.', + description: + 'Whether to use TLS for SMTP connection. Please check this in your SMTP provider settings.', category: 'Email', type: 'boolean', contextualValidation: { diff --git a/apps/dev-tool/package.json b/apps/dev-tool/package.json index 6062aff90..e7b00deb8 100644 --- a/apps/dev-tool/package.json +++ b/apps/dev-tool/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "@ai-sdk/openai": "^1.3.22", - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@tanstack/react-query": "5.80.6", "ai": "4.3.16", "lucide-react": "^0.513.0", @@ -36,7 +36,7 @@ "tailwindcss": "4.1.8", "tailwindcss-animate": "^1.0.7", "typescript": "^5.8.3", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "prettier": "@kit/prettier-config", "browserslist": [ diff --git a/apps/web/package.json b/apps/web/package.json index 6063852f7..2571c5631 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@edge-csrf/nextjs": "2.5.3-cloudflare-rc1", - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@kit/accounts": "workspace:*", "@kit/admin": "workspace:*", "@kit/analytics": "workspace:*", @@ -56,7 +56,7 @@ "@marsidev/react-turnstile": "^1.1.0", "@nosecone/next": "1.0.0-beta.8", "@radix-ui/react-icons": "^1.3.2", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@tanstack/react-query": "5.80.6", "@tanstack/react-table": "^8.21.3", "date-fns": "^4.1.0", @@ -71,7 +71,7 @@ "recharts": "2.15.3", "sonner": "^2.0.5", "tailwind-merge": "^3.3.0", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "devDependencies": { "@kit/eslint-config": "workspace:*", diff --git a/package.json b/package.json index eb9b5f9d4..1b6ea95e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-supabase-saas-kit-turbo", - "version": "2.9.0", + "version": "2.10.0", "private": true, "sideEffects": false, "engines": { diff --git a/packages/billing/core/package.json b/packages/billing/core/package.json index c5e5bf554..4c5b9e496 100644 --- a/packages/billing/core/package.json +++ b/packages/billing/core/package.json @@ -21,7 +21,7 @@ "@kit/supabase": "workspace:*", "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:*", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/billing/gateway/package.json b/packages/billing/gateway/package.json index bf789465b..3e988aaea 100644 --- a/packages/billing/gateway/package.json +++ b/packages/billing/gateway/package.json @@ -16,7 +16,7 @@ "./marketing": "./src/components/marketing.tsx" }, "devDependencies": { - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@kit/billing": "workspace:*", "@kit/eslint-config": "workspace:*", "@kit/lemon-squeezy": "workspace:*", @@ -26,7 +26,7 @@ "@kit/supabase": "workspace:*", "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:*", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@types/react": "19.1.6", "date-fns": "^4.1.0", "lucide-react": "^0.513.0", @@ -34,7 +34,7 @@ "react": "19.1.0", "react-hook-form": "^7.57.0", "react-i18next": "^15.5.2", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/billing/gateway/src/components/current-plan-alert.tsx b/packages/billing/gateway/src/components/current-plan-alert.tsx index 7effd912b..9eeca42c9 100644 --- a/packages/billing/gateway/src/components/current-plan-alert.tsx +++ b/packages/billing/gateway/src/components/current-plan-alert.tsx @@ -2,7 +2,10 @@ import { Enums } from '@kit/supabase/database'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Trans } from '@kit/ui/trans'; -const statusBadgeMap: Record, `success` | `destructive` | `warning`> = { +const statusBadgeMap: Record< + Enums<'subscription_status'>, + `success` | `destructive` | `warning` +> = { active: 'success', trialing: 'success', past_due: 'destructive', @@ -35,4 +38,4 @@ export function CurrentPlanAlert( ); -} \ No newline at end of file +} diff --git a/packages/billing/gateway/src/components/current-plan-badge.tsx b/packages/billing/gateway/src/components/current-plan-badge.tsx index 96c19d7b7..23f7ab2e6 100644 --- a/packages/billing/gateway/src/components/current-plan-badge.tsx +++ b/packages/billing/gateway/src/components/current-plan-badge.tsx @@ -16,7 +16,7 @@ const statusBadgeMap: Record = { pending: 'warning', incomplete_expired: 'destructive', paused: 'warning', -} +}; export function CurrentPlanBadge( props: React.PropsWithoutRef<{ diff --git a/packages/billing/lemon-squeezy/package.json b/packages/billing/lemon-squeezy/package.json index 597e058f8..1570fc065 100644 --- a/packages/billing/lemon-squeezy/package.json +++ b/packages/billing/lemon-squeezy/package.json @@ -27,7 +27,7 @@ "@types/react": "19.1.6", "next": "15.3.3", "react": "19.1.0", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-webhook-handler.service.ts b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-webhook-handler.service.ts index fe92f662c..f6b617b7a 100644 --- a/packages/billing/lemon-squeezy/src/services/lemon-squeezy-webhook-handler.service.ts +++ b/packages/billing/lemon-squeezy/src/services/lemon-squeezy-webhook-handler.service.ts @@ -454,7 +454,7 @@ export class LemonSqueezyWebhookHandlerService pending: 'pending', failed: 'failed', refunded: 'failed', - } satisfies Record + } satisfies Record; return statusMap[status] ?? 'pending'; } diff --git a/packages/billing/stripe/package.json b/packages/billing/stripe/package.json index 582957e2f..f6fb855a3 100644 --- a/packages/billing/stripe/package.json +++ b/packages/billing/stripe/package.json @@ -31,7 +31,7 @@ "date-fns": "^4.1.0", "next": "15.3.3", "react": "19.1.0", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/cms/keystatic/package.json b/packages/cms/keystatic/package.json index 4b4822ac2..189feca72 100644 --- a/packages/cms/keystatic/package.json +++ b/packages/cms/keystatic/package.json @@ -29,7 +29,7 @@ "@types/node": "^22.15.30", "@types/react": "19.1.6", "react": "19.1.0", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/database-webhooks/package.json b/packages/database-webhooks/package.json index 123d008e7..a038490cd 100644 --- a/packages/database-webhooks/package.json +++ b/packages/database-webhooks/package.json @@ -22,8 +22,8 @@ "@kit/supabase": "workspace:*", "@kit/team-accounts": "workspace:*", "@kit/tsconfig": "workspace:*", - "@supabase/supabase-js": "2.49.10", - "zod": "^3.25.53" + "@supabase/supabase-js": "2.50.0", + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/features/accounts/package.json b/packages/features/accounts/package.json index 848576236..c276ad0ca 100644 --- a/packages/features/accounts/package.json +++ b/packages/features/accounts/package.json @@ -20,7 +20,7 @@ "nanoid": "^5.1.5" }, "devDependencies": { - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@kit/billing-gateway": "workspace:*", "@kit/email-templates": "workspace:*", "@kit/eslint-config": "workspace:*", @@ -34,7 +34,7 @@ "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:*", "@radix-ui/react-icons": "^1.3.2", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@tanstack/react-query": "5.80.6", "@types/react": "19.1.6", "@types/react-dom": "19.1.6", @@ -46,7 +46,7 @@ "react-hook-form": "^7.57.0", "react-i18next": "^15.5.2", "sonner": "^2.0.5", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "prettier": "@kit/prettier-config", "typesVersions": { diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx index 99c5a191c..195dc374e 100644 --- a/packages/features/accounts/src/components/account-selector.tsx +++ b/packages/features/accounts/src/components/account-selector.tsx @@ -100,7 +100,7 @@ export function AccountSelector({ role="combobox" aria-expanded={open} className={cn( - 'dark:shadow-primary/10 group w-full min-w-0 px-2 lg:w-auto lg:max-w-fit mr-1', + 'dark:shadow-primary/10 group mr-1 w-full min-w-0 px-2 lg:w-auto lg:max-w-fit', { 'justify-start': !collapsed, 'm-auto justify-center px-2 lg:w-full': collapsed, diff --git a/packages/features/admin/package.json b/packages/features/admin/package.json index 848b65157..5a6cb1008 100644 --- a/packages/features/admin/package.json +++ b/packages/features/admin/package.json @@ -10,7 +10,7 @@ }, "prettier": "@kit/prettier-config", "devDependencies": { - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@kit/eslint-config": "workspace:*", "@kit/next": "workspace:*", "@kit/prettier-config": "workspace:*", @@ -20,7 +20,7 @@ "@kit/ui": "workspace:*", "@makerkit/data-loader-supabase-core": "^0.0.10", "@makerkit/data-loader-supabase-nextjs": "^1.2.5", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@tanstack/react-query": "5.80.6", "@tanstack/react-table": "^8.21.3", "@types/react": "19.1.6", @@ -29,7 +29,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.57.0", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "exports": { ".": "./src/index.ts", diff --git a/packages/features/auth/package.json b/packages/features/auth/package.json index e99856045..b1c5745b0 100644 --- a/packages/features/auth/package.json +++ b/packages/features/auth/package.json @@ -19,7 +19,7 @@ "./resend-email-link": "./src/components/resend-auth-link-form.tsx" }, "devDependencies": { - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@kit/eslint-config": "workspace:*", "@kit/prettier-config": "workspace:*", "@kit/shared": "workspace:*", @@ -28,7 +28,7 @@ "@kit/ui": "workspace:*", "@marsidev/react-turnstile": "^1.1.0", "@radix-ui/react-icons": "^1.3.2", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@tanstack/react-query": "5.80.6", "@types/react": "19.1.6", "lucide-react": "^0.513.0", @@ -36,7 +36,7 @@ "react-hook-form": "^7.57.0", "react-i18next": "^15.5.2", "sonner": "^2.0.5", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "prettier": "@kit/prettier-config", "typesVersions": { diff --git a/packages/features/notifications/package.json b/packages/features/notifications/package.json index 6753124f8..a2f9289a1 100644 --- a/packages/features/notifications/package.json +++ b/packages/features/notifications/package.json @@ -19,7 +19,7 @@ "@kit/supabase": "workspace:*", "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:*", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@tanstack/react-query": "5.80.6", "@types/react": "19.1.6", "lucide-react": "^0.513.0", diff --git a/packages/features/team-accounts/package.json b/packages/features/team-accounts/package.json index 046260add..747e34683 100644 --- a/packages/features/team-accounts/package.json +++ b/packages/features/team-accounts/package.json @@ -18,7 +18,7 @@ "nanoid": "^5.1.5" }, "devDependencies": { - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@kit/accounts": "workspace:*", "@kit/billing-gateway": "workspace:*", "@kit/email-templates": "workspace:*", @@ -32,7 +32,7 @@ "@kit/supabase": "workspace:*", "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:*", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@tanstack/react-query": "5.80.6", "@tanstack/react-table": "^8.21.3", "@types/react": "19.1.6", @@ -46,7 +46,7 @@ "react-hook-form": "^7.57.0", "react-i18next": "^15.5.2", "sonner": "^2.0.5", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "prettier": "@kit/prettier-config", "typesVersions": { diff --git a/packages/mailers/core/package.json b/packages/mailers/core/package.json index 449638b03..9ee4bda39 100644 --- a/packages/mailers/core/package.json +++ b/packages/mailers/core/package.json @@ -21,7 +21,7 @@ "@kit/shared": "workspace:*", "@kit/tsconfig": "workspace:*", "@types/node": "^22.15.30", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/mailers/nodemailer/package.json b/packages/mailers/nodemailer/package.json index 4cd5af5cc..7b8970122 100644 --- a/packages/mailers/nodemailer/package.json +++ b/packages/mailers/nodemailer/package.json @@ -21,7 +21,7 @@ "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", "@types/nodemailer": "6.4.17", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/mailers/resend/package.json b/packages/mailers/resend/package.json index c4ab9495c..d3d078e6d 100644 --- a/packages/mailers/resend/package.json +++ b/packages/mailers/resend/package.json @@ -18,7 +18,7 @@ "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", "@types/node": "^22.15.30", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/mailers/shared/package.json b/packages/mailers/shared/package.json index f65a4f93b..b6d3e3620 100644 --- a/packages/mailers/shared/package.json +++ b/packages/mailers/shared/package.json @@ -16,7 +16,7 @@ "@kit/eslint-config": "workspace:*", "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/monitoring/api/package.json b/packages/monitoring/api/package.json index 8ffadf109..f43ff1c87 100644 --- a/packages/monitoring/api/package.json +++ b/packages/monitoring/api/package.json @@ -26,7 +26,7 @@ "@kit/tsconfig": "workspace:*", "@types/react": "19.1.6", "react": "19.1.0", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/monitoring/baselime/package.json b/packages/monitoring/baselime/package.json index 66d741da9..da5c4c222 100644 --- a/packages/monitoring/baselime/package.json +++ b/packages/monitoring/baselime/package.json @@ -26,7 +26,7 @@ "@kit/tsconfig": "workspace:*", "@types/react": "19.1.6", "react": "19.1.0", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/next/package.json b/packages/next/package.json index cf10751b7..0aafe539c 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -20,9 +20,9 @@ "@kit/prettier-config": "workspace:*", "@kit/supabase": "workspace:*", "@kit/tsconfig": "workspace:*", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "next": "15.3.3", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/otp/package.json b/packages/otp/package.json index 25c24f7e7..b11900692 100644 --- a/packages/otp/package.json +++ b/packages/otp/package.json @@ -14,7 +14,7 @@ "./components": "./src/components/index.ts" }, "devDependencies": { - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@kit/email-templates": "workspace:*", "@kit/eslint-config": "workspace:*", "@kit/mailers": "workspace:*", @@ -25,13 +25,13 @@ "@kit/tsconfig": "workspace:*", "@kit/ui": "workspace:*", "@radix-ui/react-icons": "^1.3.2", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@types/react": "19.1.6", "@types/react-dom": "19.1.6", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.57.0", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/shared/src/logger/impl/console.ts b/packages/shared/src/logger/impl/console.ts index c02a654b4..1ac465980 100644 --- a/packages/shared/src/logger/impl/console.ts +++ b/packages/shared/src/logger/impl/console.ts @@ -4,6 +4,6 @@ const Logger = { warn: console.warn, debug: console.debug, fatal: console.error, -} +}; -export { Logger }; \ No newline at end of file +export { Logger }; diff --git a/packages/supabase/package.json b/packages/supabase/package.json index 75cdcfee6..6b71c01aa 100644 --- a/packages/supabase/package.json +++ b/packages/supabase/package.json @@ -25,13 +25,13 @@ "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", "@supabase/ssr": "^0.6.1", - "@supabase/supabase-js": "2.49.10", + "@supabase/supabase-js": "2.50.0", "@tanstack/react-query": "5.80.6", "@types/react": "19.1.6", "next": "15.3.3", "react": "19.1.0", "server-only": "^0.0.1", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "typesVersions": { "*": { diff --git a/packages/ui/package.json b/packages/ui/package.json index ead796f7b..446866ac0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -9,7 +9,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.0", "@radix-ui/react-accordion": "1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-avatar": "^1.1.10", @@ -53,16 +53,21 @@ "next": "15.3.3", "next-themes": "0.4.6", "prettier": "^3.5.3", - "react-day-picker": "^8.10.1", + "react-day-picker": "^9.7.0", "react-hook-form": "^7.57.0", "react-i18next": "^15.5.2", "sonner": "^2.0.5", "tailwindcss": "4.1.8", "tailwindcss-animate": "^1.0.7", "typescript": "^5.8.3", - "zod": "^3.25.53" + "zod": "^3.25.56" }, "prettier": "@kit/prettier-config", + "imports": { + "#utils": [ + "./src/lib/utils/index.ts" + ] + }, "exports": { "./accordion": "./src/shadcn/accordion.tsx", "./alert-dialog": "./src/shadcn/alert-dialog.tsx", diff --git a/packages/ui/src/shadcn/calendar.tsx b/packages/ui/src/shadcn/calendar.tsx index 030dd5a47..09b3dbb3d 100644 --- a/packages/ui/src/shadcn/calendar.tsx +++ b/packages/ui/src/shadcn/calendar.tsx @@ -2,69 +2,210 @@ import * as React from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { DayPicker } from 'react-day-picker'; +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from 'lucide-react'; +import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'; import { cn } from '../lib/utils'; -import { buttonVariants } from './button'; - -export type { DateRange } from 'react-day-picker'; -export type CalendarProps = React.ComponentProps; +import { Button, buttonVariants } from './button'; function Calendar({ className, classNames, showOutsideDays = true, + captionLayout = 'label', + buttonVariant = 'ghost', + formatters, + components, ...props -}: CalendarProps) { +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps['variant']; +}) { + const defaultClassNames = getDefaultClassNames(); + return ( svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className, + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString('default', { month: 'short' }), + ...formatters, + }} classNames={{ - months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0', - month: 'space-y-4', - caption: 'flex justify-center pt-1 relative items-center', - caption_label: 'text-sm font-medium', - nav: 'gap-x-2 flex items-center', - nav_button: cn( - buttonVariants({ variant: 'outline' }), - 'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100', + root: cn('w-fit', defaultClassNames.root), + months: cn( + 'relative flex flex-col gap-4 md:flex-row', + defaultClassNames.months, + ), + month: cn('flex w-full flex-col gap-4', defaultClassNames.month), + nav: cn( + 'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1', + defaultClassNames.nav, + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50', + defaultClassNames.button_previous, + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50', + defaultClassNames.button_next, + ), + month_caption: cn( + 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)', + defaultClassNames.month_caption, + ), + dropdowns: cn( + 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium', + defaultClassNames.dropdowns, + ), + dropdown_root: cn( + 'has-focus:border-ring border-input has-focus:ring-ring/50 relative rounded-md border shadow-xs has-focus:ring-[3px]', + defaultClassNames.dropdown_root, + ), + dropdown: cn('absolute inset-0 opacity-0', defaultClassNames.dropdown), + caption_label: cn( + 'font-medium select-none', + captionLayout === 'label' + ? 'text-sm' + : '[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5', + defaultClassNames.caption_label, + ), + table: 'w-full border-collapse', + weekdays: cn('flex', defaultClassNames.weekdays), + weekday: cn( + 'text-muted-foreground flex-1 rounded-md text-[0.8rem] font-normal select-none', + defaultClassNames.weekday, + ), + week: cn('mt-2 flex w-full', defaultClassNames.week), + week_number_header: cn( + 'w-(--cell-size) select-none', + defaultClassNames.week_number_header, + ), + week_number: cn( + 'text-muted-foreground text-[0.8rem] select-none', + defaultClassNames.week_number, ), - nav_button_previous: 'absolute left-1', - nav_button_next: 'absolute right-1', - table: 'w-full border-collapse space-y-1', - head_row: 'flex', - head_cell: - 'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]', - row: 'flex w-full mt-2', - cell: 'text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20', day: cn( - buttonVariants({ variant: 'ghost' }), - 'h-9 w-9 p-0 font-normal aria-selected:opacity-100', + 'group/day relative aspect-square h-full w-full p-0 text-center select-none [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md', + defaultClassNames.day, ), - day_selected: - 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', - day_today: 'bg-accent text-accent-foreground', - day_outside: 'text-muted-foreground opacity-50', - day_disabled: 'text-muted-foreground opacity-50', - day_range_middle: - 'aria-selected:bg-accent aria-selected:text-accent-foreground', - day_hidden: 'invisible', + range_start: cn( + 'bg-accent rounded-l-md', + defaultClassNames.range_start, + ), + range_middle: cn('rounded-none', defaultClassNames.range_middle), + range_end: cn('bg-accent rounded-r-md', defaultClassNames.range_end), + today: cn( + 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none', + defaultClassNames.today, + ), + outside: cn( + 'text-muted-foreground aria-selected:text-muted-foreground', + defaultClassNames.outside, + ), + disabled: cn( + 'text-muted-foreground opacity-50', + defaultClassNames.disabled, + ), + hidden: cn('invisible', defaultClassNames.hidden), ...classNames, }} components={{ - IconLeft: ({ ...props }) => ( - - ), - IconRight: ({ ...props }) => ( - - ), + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ); + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === 'left') { + return ( + + ); + } + + if (orientation === 'right') { + return ( + + ); + } + + return ( + + ); + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ); + }, + ...components, }} {...props} /> ); } -Calendar.displayName = 'Calendar'; -export { Calendar }; +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames(); + + const ref = React.useRef(null); + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); + + return ( +