diff --git a/.cursor/rules/after-changes.mdc b/.cursor/rules/after-changes.mdc new file mode 100644 index 000000000..81a245000 --- /dev/null +++ b/.cursor/rules/after-changes.mdc @@ -0,0 +1,38 @@ +--- +description: Always run typecheck and lint after making code changes +globs: **/*.{ts,tsx} +alwaysApply: true +--- + +# After Code Changes + +After making any code changes, **always run these checks**: + +```bash +# TypeScript type checking +bun run typecheck + +# ESLint linting +bun run lint +``` + +## Fix Errors Before Committing + +If either check fails: + +1. Fix all TypeScript errors first (they break the build) +2. Fix ESLint errors/warnings +3. Re-run checks until both pass +4. Only then commit or deploy + +## Common TypeScript Fixes + +- **Property does not exist**: Check interface/type definitions, ensure correct property names +- **Type mismatch**: Verify the expected type vs actual type being passed +- **Empty interface extends**: Use `type X = SomeType` instead of `interface X extends SomeType {}` + +## Common ESLint Fixes + +- **Unused variables**: Remove or prefix with `_` +- **Any type**: Add proper typing +- **Empty object type**: Use `type` instead of `interface` for type aliases diff --git a/.cursor/rules/better-auth.mdc b/.cursor/rules/better-auth.mdc deleted file mode 100644 index 4986ca7a2..000000000 --- a/.cursor/rules/better-auth.mdc +++ /dev/null @@ -1,28 +0,0 @@ ---- -description: When dealing with auth or authClient related code. -globs: -alwaysApply: false ---- -Only use auth.ts and auth.api methods serverside. To use them you have to pass Next.js headers as follows: -auth.api.hasPermission({ - headers: await headers(), - body: { - permissions: { - project: ["create"] // This must match the structure in your access control - } - } -}); - - - - Only use auth-client.ts and authClient on clienside. You do not need to pass headers, it's already contextually aware. - - const canCreateProject = await authClient.organization.hasPermission({ - permissions: { - project: ["create"] - } -}) - - -For the full list of methods/supported actions reference: -https://www.better-auth.com/docs/plugins/organization \ No newline at end of file diff --git a/.cursor/rules/code-standards.mdc b/.cursor/rules/code-standards.mdc new file mode 100644 index 000000000..da46b64d1 --- /dev/null +++ b/.cursor/rules/code-standards.mdc @@ -0,0 +1,186 @@ +--- +description: General code quality standards and file size limits +globs: **/*.{ts,tsx} +alwaysApply: true +--- + +# Code Standards + +## File Size Limits + +**Files must not exceed 300 lines.** When a file approaches this limit, split it. + +### ✅ How to Split Large Files + +``` +# Before: One 400-line file +components/TaskList.tsx (400 lines) + +# After: Multiple focused files +components/ +├── TaskList.tsx # Main component (~100 lines) +├── TaskListItem.tsx # Individual item (~80 lines) +├── TaskListFilters.tsx # Filter controls (~60 lines) +└── TaskListEmpty.tsx # Empty state (~40 lines) +``` + +### Splitting Strategies + +| File Type | Split By | +| --------- | -------------------------------------------- | +| Component | Extract sub-components, hooks, utils | +| Hook | Extract helper functions, split by concern | +| Utils | Group by domain (dates, strings, validation) | +| Types | Split by entity (Task, User, Organization) | + +### ❌ Warning Signs + +```tsx +// ❌ File is too long +// TaskList.tsx - 450 lines with inline helpers, multiple components + +// ❌ Multiple components in one file +export function TaskList() { ... } +export function TaskCard() { ... } // Should be separate file +export function TaskBadge() { ... } // Should be separate file +``` + +## Code Quality + +### ✅ Always Do This + +```tsx +// Early returns for readability +function processTask(task: Task | null) { + if (!task) return null; + if (task.deleted) return null; + + return ; +} + +// Descriptive names +const handleTaskComplete = (taskId: string) => { ... }; +const isTaskOverdue = (task: Task) => task.dueDate < new Date(); + +// Const arrow functions with types +const formatDate = (date: Date): string => { + return date.toLocaleDateString(); +}; +``` + +### ❌ Never Do This + +```tsx +// No early returns - deeply nested +function processTask(task) { + if (task) { + if (!task.deleted) { + return ; + } + } + return null; +} + +// Vague names +const handleClick = () => { ... }; // Click on what? +const data = fetchStuff(); // What data? + +// Function keyword when const works +function formatDate(date) { ... } +``` + +## Function Parameters + +**Use named parameters (object destructuring) for functions with 2+ parameters.** + +### ✅ Always Do This + +```tsx +// Named parameters - clear at call site +const createTask = ({ title, assigneeId, dueDate }: CreateTaskParams) => { ... }; +createTask({ title: 'Review PR', assigneeId: user.id, dueDate: tomorrow }); + +// Hook with options object +const useTasks = ({ organizationId, initialData }: UseTasksOptions) => { ... }; +const { tasks } = useTasks({ organizationId: orgId, initialData: serverTasks }); + +// Component props (always named) +function TaskCard({ task, onComplete, showDetails }: TaskCardProps) { ... } + +``` + +### ❌ Never Do This + +```tsx +// Positional parameters - unclear at call site +const createTask = (title: string, assigneeId: string, dueDate: Date) => { ... }; +createTask('Review PR', user.id, tomorrow); // What's the 2nd param? + +// Multiple positional args are confusing +const formatRange = (start: Date, end: Date, format: string, timezone: string) => { ... }; +formatRange(startDate, endDate, 'MM/dd', 'UTC'); // Hard to read + +// Boolean positional params are the worst +fetchTasks(orgId, true, false, true); // What do these booleans mean? +``` + +### Exception: Single Parameter + +```tsx +// Single param is fine as positional +const getTask = (taskId: string) => { ... }; +const formatDate = (date: Date) => { ... }; +const isOverdue = (task: Task) => { ... }; +``` + +## Accessibility + +### ✅ Always Include + +```tsx +// Interactive elements need keyboard support +
e.key === 'Enter' && handleClick()} + aria-label="Delete task" +> + +
+ +// Form inputs need labels + + + +// Images need alt text +{`${user.name}'s +``` + +## DRY Principle + +### ✅ Extract Repeated Logic + +```tsx +// Before: Duplicated validation +if (email && email.includes('@') && email.length > 5) { ... } +if (email && email.includes('@') && email.length > 5) { ... } + +// After: Extracted helper +const isValidEmail = (email: string) => + email?.includes('@') && email.length > 5; + +if (isValidEmail(email)) { ... } +``` + +## Checklist + +Before committing: + +- [ ] No file exceeds 300 lines +- [ ] Uses early returns for conditionals +- [ ] Variable/function names are descriptive +- [ ] Functions with 2+ params use named parameters +- [ ] Interactive elements have keyboard support +- [ ] No duplicated logic (DRY) +- [ ] Const arrow functions with types diff --git a/.cursor/rules/componentization.mdc b/.cursor/rules/componentization.mdc new file mode 100644 index 000000000..23fa0af67 --- /dev/null +++ b/.cursor/rules/componentization.mdc @@ -0,0 +1,439 @@ +--- +description: Componentization philosophy - reuse over repetition with TailwindCSS and shadcn/ui +globs: **/*.{ts,tsx} +alwaysApply: true +--- + +# Componentization Philosophy + +## Core Principles + +### 1. Componentize Everything + +**Reuse over repetition.** + +- If a pattern appears 2+ times, it MUST be a component +- No copy-paste UI patterns +- Every visual element should trace back to a reusable component +- Manual implementations are technical debt + +### 2. Hard Component Enforcement + +**Components are immutable contracts.** + +```tsx +// ❌ NEVER - Arbitrary className overrides on design system components + +Content +Status + +// ✅ ALWAYS - Use component variants and props + +Content +Status +``` + +**Rules:** + +- Use Tailwind utility classes in custom components, NOT on design system components +- Use shadcn/ui variant props (`variant`, `size`) - never override with className +- Compose from primitives (flex containers, semantic HTML) +- Use `cn()` utility for conditional class merging + +### 3. Extension Strategy + +**When you need different styling, extend the component properly.** + +#### Option A: Add a Variant (using shadcn patterns) + +```tsx +// In components/ui/badge.tsx - extend variants +import { cva, type VariantProps } from "class-variance-authority"; + +const badgeVariants = cva( + "inline-flex items-center rounded-md px-2 py-1 text-xs font-medium", + { + variants: { + variant: { + default: "bg-primary/10 text-primary", + secondary: "bg-secondary text-secondary-foreground", + destructive: "bg-destructive/10 text-destructive", + outline: "border border-input bg-transparent", + // ADD NEW VARIANTS HERE + counter: "bg-muted text-muted-foreground tabular-nums font-mono", + technical: "bg-teal-500/10 text-teal-600 dark:text-teal-400 font-mono", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +// Usage +03 +SYS.ID +``` + +#### Option B: Add Props for Flexibility + +```tsx +// In components/ui/text.tsx - semantic text component +import { cn } from "@/lib/utils"; + +interface TextProps extends React.HTMLAttributes { + size?: "xs" | "sm" | "base" | "lg"; + variant?: "default" | "muted" | "primary" | "destructive"; + mono?: boolean; +} + +const sizeClasses = { + xs: "text-xs", + sm: "text-sm", + base: "text-base", + lg: "text-lg", +}; + +const variantClasses = { + default: "text-foreground", + muted: "text-muted-foreground", + primary: "text-primary", + destructive: "text-destructive", +}; + +export const Text = ({ + size = "base", + variant = "default", + mono, + className, + ...props +}: TextProps) => ( +

+); + +// Usage +Technical Label +``` + +#### Option C: Create a New Component + +```tsx +// components/ui/metric-number.tsx +import { cn } from "@/lib/utils"; + +interface MetricNumberProps { + value: number; + size?: "2xl" | "3xl" | "4xl"; + variant?: "default" | "destructive" | "success"; +} + +const sizeClasses = { + "2xl": "text-2xl", + "3xl": "text-3xl", + "4xl": "text-4xl", +}; + +const variantClasses = { + default: "text-foreground", + destructive: "text-red-600 dark:text-red-400", + success: "text-green-600 dark:text-green-400", +}; + +export const MetricNumber = ({ + value, + size = "4xl", + variant = "default" +}: MetricNumberProps) => ( + + {String(value).padStart(2, "0")} + +); + +// Usage + +``` + +### 4. Reusability Through Composition + +**Build complex UIs from simple, focused components.** + +```tsx +// ❌ BAD: Monolithic custom implementation with raw divs and classes +

+
+
+
+ + TITLE + + META +
+
+ 42 +
+
+ SYS.ID +
+
+
+ +// ✅ GOOD: Composed from reusable components + + + +``` + +## Component Development Workflow + +### When You Need Custom Styling + +1. **Check existing variants** + - Review shadcn component variants (`variant`, `size`) + - Check if existing props achieve what you need + +2. **If existing variants aren't enough, extend the component** + + ```tsx + // Add new variant to the cva definition + const buttonVariants = cva("...", { + variants: { + variant: { + // existing... + newVariant: "bg-teal-500 text-white hover:bg-teal-600", // ADD HERE + }, + }, + }); + ``` + +3. **If it's a new pattern, CREATE A COMPONENT** + + ```tsx + // components/ui/your-new-component.tsx + import { cn } from "@/lib/utils"; + + interface YourNewComponentProps { + children: React.ReactNode; + variant?: "default" | "muted"; + } + + export const YourNewComponent = ({ + children, + variant = "default" + }: YourNewComponentProps) => ( +
+ {children} +
+ ); + ``` + +4. **NEVER bypass the design system** + - Don't add random className overrides + - It breaks consistency + - It creates technical debt + +## Pattern Recognition + +### If You See This Pattern 2+ Times → Componentize + +```tsx +// Repeated list item layout +
+
+ {title} + {status} +
+ +
+→ Create ListItem component + +// Repeated counter pattern +
+ LABEL + {count} +
+→ Create CounterBadge component + +// Repeated status dot +
+→ Create StatusDot component + +// Repeated large numbers + + {String(value).padStart(2, "0")} + +→ Create MetricNumber component +``` + +## Testing Component Compliance + +### Before Committing, Check: + +- [ ] Uses shadcn/ui base components without className overrides +- [ ] Semantic props exposed (`variant`, `size`) +- [ ] No arbitrary style overrides on design system components +- [ ] Repeated patterns extracted to components +- [ ] Works in light and dark mode (uses semantic tokens) +- [ ] Uses Tailwind responsive prefixes (`sm:`, `md:`, `lg:`) +- [ ] Proper TypeScript types with inference where possible + +## Anti-Patterns to Avoid + +### ❌ Never Do This + +```tsx +// 1. Raw inline styles +
Content
+ +// 2. Overriding design system components with className + + +// 3. Hardcoded colors instead of semantic tokens +
+ +// 4. Mixing approaches + + +// 5. Copy-paste UI patterns +// If you copy UI code, STOP and componentize it first + +// 6. Non-responsive hardcoded values +
+``` + +### ✅ Always Do This + +```tsx +// 1. Use component variants + +Status +Content -- **Moderate Text Sizes**: Avoid overly large text - prefer `text-base`, `text-sm`, `text-xs` over `text-xl+` -- **Consistent Hierarchy**: Use `font-medium`, `font-semibold` sparingly, prefer `font-normal` with size differentiation -- **Tabular Numbers**: Use `tabular-nums` class for numeric data to ensure proper alignment +// Use semantic color tokens +
+
+
+ +// Dark mode with explicit variants +
+``` + +### ❌ Never Do This + +```tsx +// Overriding shadcn components with className + +Content + +// Hardcoded colors +
+
+
+``` ## Layout & Spacing -- **Flexbox-First**: ALWAYS prefer flexbox with `gap` over hardcoded margins (`mt-`, `mb-`, `ml-`, `mr-`) -- **Use Gaps, Not Margins**: Use `gap-2`, `gap-4`, `space-y-4` for spacing between elements -- **Consistent Spacing**: Use standard Tailwind spacing scale (`space-y-4`, `gap-6`, etc.) -- **Card-Based Layouts**: Prefer Card components for content organization -- **Minimal Padding**: Use conservative padding - `p-3`, `p-4` rather than larger values -- **Clean Separators**: Use subtle borders (`border-t`, `border-muted`) instead of heavy dividers -- **NEVER Hardcode Margins**: Avoid `mt-4`, `mb-2`, `ml-3` unless absolutely necessary for exceptions - -## Color & Visual Elements - -- **Status Colors**: - - Green for completed/success states - - Blue for in-progress/info states - - Yellow for warnings - - Red for errors/destructive actions -- **Subtle Indicators**: Use small colored dots (`w-2 h-2 rounded-full`) instead of large icons for status -- **Minimal Shadows**: Prefer `hover:shadow-sm` over heavy shadow effects -- **Progress Bars**: Keep thin (`h-1`, `h-2`) for minimal visual weight +### ✅ Always Do This -## Interactive Elements +```tsx +// Flexbox with gap +
+
+
-- **Subtle Hover States**: Use gentle transitions (`transition-shadow`, `hover:shadow-sm`) -- **Consistent Button Sizing**: Prefer `size="sm"` for most buttons, `size="icon"` for icon-only -- **Badge Usage**: Keep badges minimal with essential info only (percentages, short status) +// Responsive prefixes +
+
+``` -## Data Display +### ❌ Never Do This -- **Shared Design Language**: Ensure related components (cards, overviews, details) use consistent patterns -- **Minimal Stats**: Present data cleanly without excessive decoration -- **Contextual Icons**: Use small, relevant icons (`h-3 w-3`, `h-4 w-4`) sparingly for context +```tsx +// Hardcoded margins +
+ + + +// ❌ Heavy shadows and borders + +``` + +## Icons + +```tsx +// ✅ Small, contextual icons + + + +// ❌ Oversized icons + +``` + +## Exceptions + +The ONLY time you can pass className to a shadcn component: + +1. **Width utilities**: `w-full`, `max-w-md`, `flex-1` +2. **Responsive display**: `hidden`, `md:block`, `lg:flex` +3. **Grid/flex positioning**: `col-span-2`, `justify-self-end` -## Data Display +```tsx +// ✅ Acceptable className usage + +Spanning Card +Desktop Only +``` -- **Shared Design Language**: Ensure related components (cards, overviews, details) use consistent patterns -- **Minimal Stats**: Present data cleanly without excessive decoration -- **Contextual Icons**: Use small, relevant icons (`h-3 w-3`, `h-4 w-4`) sparingly for context +## Checklist -## Anti-Patterns to Avoid +Before committing UI code: -- Large text sizes (`text-2xl+` except for main headings) -- Heavy shadows or borders -- Excessive use of colored backgrounds -- Redundant badges or status indicators -- Complex custom styling overrides -- Non-semantic color usage (hardcoded hex values) -- Cluttered layouts with too many visual elements +- [ ] Uses shadcn component variants (not className overrides) +- [ ] Uses semantic color tokens (`bg-background`, `text-foreground`) +- [ ] Works in both light and dark mode +- [ ] Uses `gap` instead of margins for spacing +- [ ] Text sizes are moderate (`text-sm`, `text-base`) +- [ ] Icons are small (`h-4 w-4` or smaller) +- [ ] No hardcoded colors or arbitrary values +- [ ] Responsive with Tailwind prefixes (`md:`, `lg:`) diff --git a/.cursor/rules/file-structure.mdc b/.cursor/rules/file-structure.mdc index 2945a163b..d3d166e2c 100644 --- a/.cursor/rules/file-structure.mdc +++ b/.cursor/rules/file-structure.mdc @@ -1,8 +1,121 @@ --- -description: -globs: +description: File structure and colocalization guidelines for Next.js app router +globs: **/*.{ts,tsx} alwaysApply: true --- -Always opt for colocallizing code, where components, actions, etc.. are encapsulated at a route/page level in their own directory. If you encounter examples of non-colocalized code, try to colocalize it. -The only exception to this rule is code that will be reused across many different components/pages. \ No newline at end of file +# File Structure + +## Core Principle + +**Colocate code at the route/page level.** Components, hooks, actions, and queries should live next to the page that uses them. + +## Directory Structure + +### ✅ Good: Colocated Structure + +``` +app/(app)/[orgId]/tasks/ +├── page.tsx # Server component +├── components/ +│ ├── TaskList.tsx # Client component +│ ├── TaskCard.tsx +│ └── TaskFilters.tsx +├── hooks/ +│ └── useTasks.ts # SWR hook with mutations +├── data/ +│ └── queries.ts # Server-side queries +└── actions/ + └── task-actions.ts # If needed +``` + +### ❌ Bad: Scattered Structure + +``` +app/(app)/[orgId]/tasks/page.tsx +components/tasks/TaskList.tsx # ❌ Far from page +components/tasks/TaskCard.tsx # ❌ Hard to find +hooks/useTasks.ts # ❌ Not colocated +lib/queries/task-queries.ts # ❌ Disconnected +``` + +## When to Colocate vs Share + +### ✅ Colocate When + +- Component is only used by one page +- Hook fetches data specific to one route +- Query is only called from one page + +``` +app/(app)/[orgId]/vendors/ +├── components/ +│ └── VendorTable.tsx # Only used on vendors page +├── hooks/ +│ └── useVendors.ts # Only used here +└── page.tsx +``` + +### ✅ Share When + +- Component is used across 3+ pages +- Utility is genuinely generic +- Hook is reused across multiple routes + +``` +# Shared components live in: +src/components/ui/ # shadcn primitives +src/components/shared/ # cross-page components + +# Shared hooks live in: +src/hooks/ # e.g., useApiSWR, useDebounce +``` + +## Rules + +1. **Default to colocating** - Start by putting files next to the page +2. **Move to shared only when reused** - Don't pre-optimize +3. **Remove unused imports** - Always clean up after refactoring +4. **One component per file** - Name file same as component + +## Examples + +### ✅ Creating a New Feature + +```tsx +// app/(app)/[orgId]/risks/page.tsx +import { RiskTable } from './components/RiskTable'; +import { getRisks } from './data/queries'; + +export default async function RisksPage({ params }) { + const { orgId } = await params; + const risks = await getRisks(orgId); + return ; +} +``` + +```tsx +// app/(app)/[orgId]/risks/components/RiskTable.tsx +'use client'; +import { useRisks } from '../hooks/useRisks'; +// ... component implementation +``` + +### ❌ Anti-Pattern: Global Everything + +```tsx +// Don't do this: +import { RiskTable } from '@/components/risks/RiskTable'; +import { useRisks } from '@/hooks/useRisks'; +import { getRisks } from '@/lib/queries/risks'; +``` + +## Checklist + +Before committing: + +- [ ] New components live next to their page +- [ ] Hooks are colocated with the page that uses them +- [ ] Shared code is genuinely used by 3+ pages +- [ ] No unused imports remain +- [ ] File names match component/hook names diff --git a/.cursor/rules/forms.mdc b/.cursor/rules/forms.mdc new file mode 100644 index 000000000..0e99b10dd --- /dev/null +++ b/.cursor/rules/forms.mdc @@ -0,0 +1,423 @@ +--- +description: Form handling with React Hook Form, Zod validation, and shadcn/ui +globs: **/*.{ts,tsx} +alwaysApply: true +--- + +# Forms: React Hook Form + Zod + +## Core Principle + +**All forms MUST use React Hook Form with Zod validation. No exceptions.** + +## Required Stack + +- `react-hook-form` - Form state management +- `zod` - Schema validation +- `@hookform/resolvers` - Zod resolver for RHF + +## Basic Pattern + +```tsx +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; + +// 1. Define schema with Zod +const formSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string().min(8, 'Password must be at least 8 characters'), +}); + +// 2. Infer TypeScript type from schema +type FormData = z.infer; + +// 3. Use in component +function MyForm() { + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + const onSubmit = async (data: FormData) => { + // data is fully typed and validated + await submitToApi(data); + }; + + return ( +
+
+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+ + +
+ ); +} +``` + +## Schema Best Practices + +### String Validation + +```tsx +const schema = z.object({ + // Required string with min length + name: z.string().min(1, 'Name is required'), + + // Email validation + email: z.string().email('Invalid email'), + + // URL validation + website: z.string().url('Invalid URL'), + + // Optional string + bio: z.string().optional(), + + // String with regex + slug: z.string().regex(/^[a-z0-9-]+$/, 'Only lowercase letters, numbers, and hyphens'), + + // Trimmed string (removes whitespace) + title: z.string().trim().min(1, 'Title is required'), +}); +``` + +### Number Validation + +```tsx +const schema = z.object({ + // Integer with range + age: z.number().int().min(0).max(150), + + // Positive number + price: z.number().positive('Must be positive'), + + // Coerce string to number (for inputs) + quantity: z.coerce.number().int().min(1), +}); +``` + +### Array & Object Validation + +```tsx +const schema = z.object({ + // Array of strings + tags: z.array(z.string()).min(1, 'At least one tag required'), + + // Array of objects + items: z.array( + z.object({ + name: z.string(), + quantity: z.number(), + }) + ), + + // Nested object + address: z.object({ + street: z.string(), + city: z.string(), + zip: z.string(), + }), +}); +``` + +### Conditional Validation + +```tsx +const schema = z + .object({ + hasAccount: z.boolean(), + accountId: z.string().optional(), + }) + .refine((data) => !data.hasAccount || data.accountId, { + message: 'Account ID required when has account', + path: ['accountId'], + }); +``` + +## shadcn/ui Form Components + +### Basic Input + +```tsx +
+ + + {errors.name && ( +

{errors.name.message}

+ )} +
+``` + +### With Controller (for complex components) + +```tsx +import { Controller } from 'react-hook-form'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; + + ( + + )} +/> +``` + +### Textarea + +```tsx +import { Textarea } from '@/components/ui/textarea'; + +
+ +