# YCode Cursor AI Rules

## 🎯 Project Context

**YCode** is a self-hosted visual website builder built with Next.js 15, TypeScript, Tailwind CSS, and Supabase. This is a **fast-moving team project** where multiple developers use Cursor simultaneously. Code consistency and abstraction are critical.

---

## 🧠 Core Philosophy: ALWAYS Think About Abstraction

Before writing any code, ask yourself:
1. **Can this logic be extracted into a reusable hook?**
2. **Does a ShadCN component already exist for this UI element?**
3. **Is this component doing too many things? Should it be split?**
4. **Can this be a pure utility function instead of component logic?**
5. **Will another team member be able to reuse this easily?**

**Golden Rule**: If you're writing the same code twice, abstract it.

---

## 🎨 1. ShadCN Components (MANDATORY)

### ALWAYS Use ShadCN for UI Primitives

**Available ShadCN Components** (in `/components/ui/`):
- `Button` - All button interactions
- `Input` - Text inputs
- `Textarea` - Multi-line text
- `Label` - Form labels
- `Select` - Dropdowns
- `Tabs` - Tab navigation
- `Dropdown Menu` - Context menus
- `Popover` - Overlays
- `Tooltip` - Hover hints
- `Slider` - Range inputs
- `Badge` - Status indicators
- `Spinner` - Loading states
- `Icon` - Lucide icons (via icon.tsx)

### ✅ CORRECT Usage

```typescript
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Icon } from '@/components/ui/icon';

export function SearchBar({ onSearch }: SearchBarProps) {
  const [query, setQuery] = useState('');

  return (
    <div className="flex gap-2">
      <Input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search pages..."
      />
      <Button size="sm" onClick={() => onSearch(query)}>
        <Icon name="search" />
        Search
      </Button>
    </div>
  );
}
```

### ❌ FORBIDDEN - Never Do This

```typescript
// ❌ WRONG: Custom button instead of ShadCN
export function SearchBar({ onSearch }: SearchBarProps) {
  return (
    <button className="px-4 py-2 bg-blue-500 rounded hover:bg-blue-600">
      Search
    </button>
  );
}

// ❌ WRONG: Custom input instead of ShadCN
<input className="border rounded px-2 py-1" />
```

### Extending ShadCN Components

If you need custom behavior, **compose** ShadCN components:

```typescript
// ✅ CORRECT: Composition
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';

interface IconButtonProps {
  icon: string;
  label: string;
  onClick: () => void;
}

export function IconButton({ icon, label, onClick }: IconButtonProps) {
  return (
    <Button size="sm" variant="secondary" onClick={onClick}>
      <Icon name={icon} />
      {label}
    </Button>
  );
}
```

---

## 🏗️ 2. Component Architecture & Abstraction

### Single Responsibility Principle

Each component should do ONE thing well:

```typescript
// ✅ GOOD: Small, focused components
function PageListItem({ page, onSelect, isActive }: PageListItemProps) {
  return (
    <button
      onClick={() => onSelect(page.id)}
      className={cn(
        'w-full text-left px-3 py-2 rounded',
        isActive ? 'bg-zinc-700' : 'hover:bg-zinc-800'
      )}
    >
      <Icon name="file" />
      {page.title}
    </button>
  );
}

function PageList({ pages, selectedId, onSelect }: PageListProps) {
  return (
    <div className="flex flex-col gap-1">
      {pages.map(page => (
        <PageListItem
          key={page.id}
          page={page}
          isActive={selectedId === page.id}
          onSelect={onSelect}
        />
      ))}
    </div>
  );
}

// ❌ BAD: Monolithic component doing everything
function PageManagement() {
  // 500 lines of code managing pages, settings, modals, etc.
}
```

### Extract Custom Hooks

Move reusable logic into custom hooks:

```typescript
// ✅ GOOD: Reusable hook
export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}

// Usage in component
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    if (debouncedQuery) {
      performSearch(debouncedQuery);
    }
  }, [debouncedQuery]);
}
```

### Extract Utility Functions

Keep components clean by moving pure functions to `/lib`:

```typescript
// ✅ GOOD: Utility in /lib/layer-utils.ts
export function findLayerById(layers: Layer[], id: string): Layer | null {
  for (const layer of layers) {
    if (layer.id === id) return layer;
    if (layer.children) {
      const found = findLayerById(layer.children, id);
      if (found) return found;
    }
  }
  return null;
}

// Usage in component
import { findLayerById } from '@/lib/layer-utils';

function LayerEditor({ layers, selectedId }: LayerEditorProps) {
  const selectedLayer = findLayerById(layers, selectedId);
  // ...
}
```

---

## 📁 3. File & Naming Conventions

### File Names
- **All files**: `kebab-case.tsx` (e.g., `page-settings.tsx`, `user-profile.tsx`)
- **Components**: PascalCase export matching filename
  - File: `layer-renderer.tsx` → Export: `LayerRenderer`
- **Utilities**: camelCase functions
  - File: `layer-utils.ts` → Export: `findLayerById`
- **Hooks**: camelCase with `use` prefix
  - File: `use-debounce.ts` → Export: `useDebounce`

### Variable & Function Names

```typescript
// Components & Types: PascalCase
const UserProfile: React.FC<UserProfileProps> = () => {};
interface LayerProps {}
type PageStatus = 'draft' | 'published';

// Functions & Variables: camelCase
const handleClick = () => {};
const userData = fetchUser();
function formatDate(date: Date) {}

// Event Handlers: handleX pattern
const handleSubmit = () => {};
const handlePageSelect = (id: string) => {};
const handleInputChange = (e: ChangeEvent) => {};

// Boolean: isX, hasX, shouldX
const isLoading = false;
const hasChildren = layers.length > 0;
const shouldRender = isVisible && hasData;

// Constants: SCREAMING_SNAKE_CASE
const MAX_PAGE_SIZE = 100;
const API_BASE_URL = 'https://api.example.com';
const DEFAULT_LAYER_CLASSES = 'flex gap-4';
```

---

## 📦 4. Import Organization

**ALWAYS order imports consistently** to prevent merge conflicts:

```typescript
// 1. React/Next.js core
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import Image from 'next/image';

// 2. External libraries (alphabetical)
import { create } from 'zustand';
import debounce from 'lodash.debounce';
import { DndContext } from '@dnd-kit/core';

// 3. ShadCN UI components (alphabetical)
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';

// 4. Internal components (alphabetical)
import { LayerRenderer } from '@/components/layers/LayerRenderer';
import { PageSettings } from './PageSettings';
import ElementLibrary from './ElementLibrary';

// 5. Stores (alphabetical)
import { useEditorStore } from '@/stores/useEditorStore';
import { usePagesStore } from '@/stores/usePagesStore';

// 6. Utils, APIs, lib (alphabetical)
import { pagesApi } from '@/lib/api';
import { cn } from '@/lib/utils';
import { findLayerById } from '@/lib/layer-utils';

// 7. Types (always last, alphabetical)
import type { Layer, Page, PageLayers, Asset } from '@/types';
```

---

## 🛣️ 5. Path Aliases - When to Use What

### Rules:
- Use `@/` for **everything outside current directory**
- Use `./` **ONLY** for files in the **same directory**
- Use `../` **sparingly** (max 1 level up, prefer `@/`)
- **NEVER** use deep relative paths like `../../../../`

```typescript
// ✅ CORRECT
import { Button } from '@/components/ui/button';
import { useEditorStore } from '@/stores/useEditorStore';
import { PageSettings } from './PageSettings'; // Same directory
import { findLayerById } from '@/lib/layer-utils';

// ❌ WRONG
import { Button } from '../../../components/ui/button';
import { useEditorStore } from '../../stores/useEditorStore';
```

---

## ⚛️ 6. Server vs Client Components (Next.js 15)

### Default: Server Components
**Don't add any directive** - components are Server Components by default.

### Add `'use client'` ONLY When:
- Using React hooks (`useState`, `useEffect`, `useCallback`, etc.)
- Using event handlers (`onClick`, `onChange`, `onSubmit`)
- Using browser APIs (`window`, `localStorage`, `document`)
- Using Zustand stores
- Using client-only libraries

```typescript
// ✅ Server Component (no directive)
// app/[slug]/page.tsx
export default async function Page({ params }: { params: { slug: string } }) {
  const page = await fetchPageBySlug(params.slug);
  return <LayerRenderer layers={page.layers} />;
}

// ✅ Client Component (with directive)
// app/ycode/components/LayersTree.tsx
'use client';

import { useState } from 'react';

export default function LayersTree({ layers }: LayersTreeProps) {
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
  // Component using hooks and state
}
```

### Keep `'use client'` Close to Leaves
Push client boundaries down the tree:

```typescript
// ✅ GOOD: Server component wraps client component
// app/page.tsx (Server Component)
export default function Page() {
  return (
    <div>
      <Header /> {/* Server */}
      <InteractiveWidget /> {/* Client */}
      <Footer /> {/* Server */}
    </div>
  );
}

// ❌ BAD: Everything is client-side unnecessarily
'use client';

export default function Page() {
  return (
    <div>
      <Header /> {/* Unnecessarily client-side */}
      <InteractiveWidget />
      <Footer /> {/* Unnecessarily client-side */}
    </div>
  );
}
```

---

## 🗄️ 7. Repository Pattern (Backend/API)

### NEVER Use Supabase Directly in API Routes

**Architecture**: `API Route → Repository → Supabase Client`

```typescript
// ✅ CORRECT: Use repository pattern
// app/api/pages/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getPageById, updatePage } from '@/lib/repositories/pageRepository';

export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const page = await getPageById(params.id);

    if (!page) {
      return NextResponse.json({ error: 'Page not found' }, { status: 404 });
    }

    return NextResponse.json({ data: page });
  } catch (error) {
    console.error('Error fetching page:', error);
    return NextResponse.json(
      { error: 'Failed to fetch page' },
      { status: 500 }
    );
  }
}

// ❌ WRONG: Direct Supabase in API route
import { getSupabaseAdmin } from '@/lib/supabase-server';

export async function GET(request: NextRequest, { params }) {
  const supabase = await getSupabaseAdmin();
  const { data } = await supabase.from('pages').select('*').eq('id', params.id);
  // WRONG! Use repository instead
}
```

### Create Repositories for All Data Entities

**IMPORTANT**: Always use alias imports (`@/lib/...`) for all lib modules including `supabase-server` and repositories. Never use relative imports like `../supabase-server` or `../repositories/settingsRepository`. This ensures consistent behavior and allows overlays to work correctly in multi-tenant deployments.

Example structure in `/lib/repositories/pageRepository.ts`:

```typescript
import { getSupabaseAdmin } from '@/lib/supabase-server';
import type { Page } from '@/types';

export async function getAllPages(): Promise<Page[]> {
  const client = await getSupabaseAdmin();
  const { data, error } = await client
    .from('pages')
    .select('*')
    .order('created_at', { ascending: false });

  if (error) throw new Error(`Failed to fetch pages: ${error.message}`);
  return data || [];
}

export async function getPageById(id: string): Promise<Page | null> {
  const client = await getSupabaseAdmin();
  const { data, error } = await client
    .from('pages')
    .select('*')
    .eq('id', id)
    .single();

  if (error && error.code !== 'PGRST116') {
    throw new Error(`Failed to fetch page: ${error.message}`);
  }

  return data;
}

export async function createPage(pageData: Omit<Page, 'id' | 'created_at' | 'updated_at'>): Promise<Page> {
  const client = await getSupabaseAdmin();
  const { data, error } = await client
    .from('pages')
    .insert(pageData)
    .select()
    .single();

  if (error) throw new Error(`Failed to create page: ${error.message}`);
  return data;
}
```

### ALWAYS Use Upsert for Database Operations

**NEVER execute queries in loops or use Promise.all for individual database operations.**

Use batch operations with `upsert` instead of separate insert/update operations.

```typescript
// ✅ CORRECT: Batch upsert operation
const itemsToUpsert = items.map(item => ({
  id: item.id,
  name: item.name,
  is_published: true,
  updated_at: new Date().toISOString(),
}));

await client
  .from('table_name')
  .upsert(itemsToUpsert, {
    onConflict: 'id,is_published', // Composite key
  });

// ❌ WRONG: Queries in loops
for (const item of items) {
  const existing = await getItemById(item.id);
  if (existing) {
    await client.from('table_name').update(item).eq('id', item.id);
  } else {
    await client.from('table_name').insert(item);
  }
}

// ❌ WRONG: Promise.all with individual queries
const updates = items.map(item =>
  client.from('table_name').update(item).eq('id', item.id)
);
await Promise.all(updates);
```

### Rules
1. **ALWAYS** use `upsert` for insert/update operations
2. **NEVER** execute queries inside loops
3. **NEVER** use `Promise.all` with individual update queries
4. **ALWAYS** batch operations when possible
5. Use `onConflict` parameter to specify composite keys for upsert operations

---

## 🔄 8. API Response Format (Consistency)

### All API Routes MUST Return Consistent Format

Use the `ApiResponse<T>` type from `/types/index.ts`:

```typescript
// ✅ Success response
return NextResponse.json({
  data: result
}, { status: 200 });

// ✅ Error response
return NextResponse.json({
  error: 'Descriptive error message'
}, { status: 400 });

// ✅ With optional message
return NextResponse.json({
  data: result,
  message: 'Page published successfully'
}, { status: 200 });
```

### Status Codes
- `200` - Success (GET, PUT)
- `201` - Created (POST)
- `204` - No Content (DELETE)
- `400` - Bad Request (validation errors)
- `404` - Not Found
- `500` - Internal Server Error

---

## ⚠️ 9. Error Handling & Async Operations

### ALWAYS Follow This Pattern

```typescript
// ✅ REQUIRED pattern for ALL async operations
const handleAction = async () => {
  try {
    setLoading(true);
    setError(null); // Clear previous errors

    const result = await apiCall();

    // Success handling
    setData(result);
  } catch (error) {
    console.error('Context-specific message:', error);
    setError(error instanceof Error ? error.message : 'Unknown error occurred');
  } finally {
    setLoading(false); // Always runs
  }
};
```

### In Components with UI State

```typescript
function PageEditor({ pageId }: PageEditorProps) {
  const [page, setPage] = useState<Page | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const loadPage = async () => {
      try {
        setLoading(true);
        const result = await pagesApi.getById(pageId);
        setPage(result.data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to load page');
      } finally {
        setLoading(false);
      }
    };

    loadPage();
  }, [pageId]);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!page) return null;

  return <div>{/* Render page */}</div>;
}
```

---

## ⏳ 10. Loading States (MANDATORY)

### Always Show Loading for Async Operations

```typescript
// ✅ CORRECT: Loading state shown
function PageList() {
  const [pages, setPages] = useState<Page[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchPages().then(setPages).finally(() => setLoading(false));
  }, []);

  if (loading) {
    return <Spinner />;
  }

  return <div>{/* Render pages */}</div>;
}

// ❌ WRONG: No loading state
function PageList() {
  const [pages, setPages] = useState<Page[]>([]);

  useEffect(() => {
    fetchPages().then(setPages);
  }, []);

  return <div>{/* Flashes empty state, then content */}</div>;
}
```

### Use ShadCN Spinner

```typescript
import { Spinner } from '@/components/ui/spinner';

if (loading) return <Spinner />;
if (loading) return (
  <div className="flex items-center justify-center p-8">
    <Spinner />
  </div>
);
```

---

## 🔀 11. Conditional Rendering - Early Returns

### Prefer Early Returns Over Nested Ternaries

```typescript
// ✅ GOOD: Early returns (clean, readable)
function PageEditor({ pageId }: PageEditorProps) {
  const { page, loading, error } = usePageData(pageId);

  if (loading) {
    return <Spinner />;
  }

  if (error) {
    return <ErrorMessage error={error} />;
  }

  if (!page) {
    return <EmptyState message="Page not found" />;
  }

  return (
    <div>
      <h1>{page.title}</h1>
      {/* Main content */}
    </div>
  );
}

// ❌ BAD: Nested ternaries (hard to read, hard to maintain)
function PageEditor({ pageId }: PageEditorProps) {
  const { page, loading, error } = usePageData(pageId);

  return loading ? (
    <Spinner />
  ) : error ? (
    <ErrorMessage error={error} />
  ) : !page ? (
    <EmptyState message="Page not found" />
  ) : (
    <div>
      <h1>{page.title}</h1>
      {/* Main content */}
    </div>
  );
}
```

### For Simple Conditions

Use inline ternaries for simple show/hide:

```typescript
// ✅ GOOD: Simple inline conditional
<div>
  {isVisible && <Notice message="Page is published" />}
  {count > 0 ? <Badge>{count}</Badge> : null}
</div>
```

---

## 🎭 12. State Management with Zustand

### Keep Stores Focused and Domain-Specific

```typescript
// ✅ GOOD: Focused store
// stores/useEditorStore.ts
import { create } from 'zustand';

interface EditorState {
  selectedLayerId: string | null;
  currentPageId: string | null;
  isLoading: boolean;
  isSaving: boolean;
}

interface EditorActions {
  setSelectedLayerId: (id: string | null) => void;
  setCurrentPageId: (id: string | null) => void;
  setLoading: (value: boolean) => void;
  setSaving: (value: boolean) => void;
}

type EditorStore = EditorState & EditorActions;

export const useEditorStore = create<EditorStore>((set) => ({
  selectedLayerId: null,
  currentPageId: null,
  isLoading: false,
  isSaving: false,

  setSelectedLayerId: (id) => set({ selectedLayerId: id }),
  setCurrentPageId: (id) => set({ currentPageId: id }),
  setLoading: (value) => set({ isLoading: value }),
  setSaving: (value) => set({ isSaving: value }),
}));
```

### Use TypeScript Interfaces for Store Types

Always define separate interfaces for state and actions, then combine them.

---

## ⚡ 13. Performance Optimization

### When to Optimize

1. **Profile First** - Don't optimize prematurely
2. **Use React DevTools Profiler** to identify slow components
3. **Optimize when you see actual performance issues**

### React.memo for Expensive Pure Components

```typescript
// ✅ Use React.memo for pure components that render often
import React, { memo } from 'react';

interface LayerItemProps {
  layer: Layer;
  onSelect: (id: string) => void;
}

const LayerItem: React.FC<LayerItemProps> = memo(({ layer, onSelect }) => {
  return (
    <div onClick={() => onSelect(layer.id)}>
      {layer.name}
    </div>
  );
});
```

### useCallback for Functions Passed as Props

```typescript
// ✅ useCallback prevents unnecessary re-renders
function LayersList({ layers }: LayersListProps) {
  const handleSelect = useCallback((id: string) => {
    console.log('Selected:', id);
  }, []); // Empty deps - function never changes

  return (
    <>
      {layers.map(layer => (
        <LayerItem key={layer.id} layer={layer} onSelect={handleSelect} />
      ))}
    </>
  );
}
```

### useMemo for Expensive Calculations

```typescript
// ✅ useMemo caches expensive computations
function PageEditor({ pageId }: PageEditorProps) {
  const { draftsByPageId } = usePagesStore();

  // Expensive: traverse tree structure
  const layersForCurrentPage = useMemo(() => {
    if (!pageId) return [];
    const draft = draftsByPageId[pageId];
    return draft ? draft.layers : [];
  }, [pageId, draftsByPageId]);

  return <LayerRenderer layers={layersForCurrentPage} />;
}
```

---

## ♿ 14. Accessibility (A11y)

### Required for All Interactive Elements

```typescript
// ✅ GOOD: Accessible button
<Button
  onClick={handleDelete}
  aria-label="Delete page"
  aria-describedby="delete-hint"
>
  <Icon name="trash" />
</Button>
<span id="delete-hint" className="sr-only">
  This action cannot be undone
</span>

// ❌ BAD: Not accessible
<div onClick={handleDelete}>
  <Icon name="trash" />
</div>
```

### Checklist
- ✅ Use semantic HTML (`<button>` not `<div onClick>`)
- ✅ Include ARIA labels for icon-only buttons
- ✅ Maintain proper heading hierarchy (h1 → h2 → h3)
- ✅ Add alt text for images
- ✅ Ensure keyboard navigation works (Tab, Enter, Esc)
- ✅ Test with screen reader when possible
- ✅ Maintain sufficient color contrast

---

## 📝 15. Comments & Documentation

### When to Comment

```typescript
// ✅ GOOD: Explain WHY, not WHAT
// Use debounce to prevent excessive API calls during typing
const debouncedSearch = useDebounce(searchQuery, 500);

// Edge case: Supabase returns error code PGRST116 for "not found"
if (error && error.code !== 'PGRST116') {
  throw error;
}

// TODO: Add pagination after 100 pages - performance degrades with large lists
const pages = await getAllPages();

// ❌ BAD: Obvious comments that repeat the code
// Set loading to true
setLoading(true);

// Loop through pages
pages.forEach(page => {
  // ...
});
```

### JSDoc for Public APIs

```typescript
/**
 * Recursively finds a layer by ID in a tree structure
 * @param layers - Array of layers to search
 * @param id - Unique layer ID to find
 * @returns The found layer or null if not found
 */
export function findLayerById(layers: Layer[], id: string): Layer | null {
  for (const layer of layers) {
    if (layer.id === id) return layer;
    if (layer.children) {
      const found = findLayerById(layer.children, id);
      if (found) return found;
    }
  }
  return null;
}
```

---

## 🧹 16. Input Sanitization (MANDATORY for Design Properties)

### ALWAYS Sanitize User Input for Tailwind Classes

Design property inputs (fontSize, width, padding, etc.) MUST be sanitized to prevent invalid Tailwind classes. Spaces break Tailwind syntax.

### Use Centralized Utilities

**Location**: `/lib/utils.ts`

```typescript
import { removeSpaces, sanitizeInput } from '@/lib/utils';

// ✅ CORRECT: Use removeSpaces for Tailwind values
const handleFontSizeChange = (value: string) => {
  const sanitized = removeSpaces(value); // "10 rem" → "10rem"
  updateDesignProperty('typography', 'fontSize', sanitized || null);
};

// ✅ CORRECT: Use controlled input hook (auto-sanitizes)
const [fontSizeInput, setFontSizeInput] = useControlledInput(fontSize);
// Spaces automatically stripped at input level - never appear in the field

// ❌ WRONG: No sanitization
const handleChange = (value: string) => {
  updateDesignProperty('typography', 'fontSize', value); // "10 rem" creates invalid class!
};
```

### useControlledInput Hook

The `useControlledInput` hook automatically sanitizes input values by default:

```typescript
import { useControlledInput } from '@/hooks/use-controlled-input';

// Auto-sanitizes by default (removes spaces)
const [fontSizeInput, setFontSizeInput] = useControlledInput(fontSize);

// Opt-out of sanitization if needed (rare)
const [customInput, setCustomInput] = useControlledInput(value, undefined, false);
```

### Available Sanitization Functions

```typescript
// Quick helper: Remove all spaces
removeSpaces('10 rem')  // '10rem'

// Advanced: Custom sanitization
sanitizeInput('  100px  ', {
  removeSpaces: false,
  trim: true  // '100px'
})

sanitizeInput('#FF 00 00', {
  removeSpaces: true  // '#FF0000'
})
```

### Rules

1. **ALWAYS** use `removeSpaces()` from `@/lib/utils` for design property handlers
2. **NEVER** create duplicate `const sanitize =` helpers in components
3. **USE** `useControlledInput` hook for design property inputs (auto-sanitizes)
4. **IMPORT** from `@/lib/utils`, not inline implementations

### Defense in Depth

We use **two layers** of protection:

1. **Hook Level**: `useControlledInput` strips spaces before they appear in the input field
2. **Handler Level**: `removeSpaces()` ensures spaces don't get saved

### Example: Complete Implementation

```typescript
import { useControlledInput } from '@/hooks/use-controlled-input';
import { removeSpaces } from '@/lib/utils';
import { extractMeasurementValue } from '@/lib/measurement-utils';

export default function TypographyControls({ layer, onLayerUpdate }) {
  const { updateDesignProperty, getDesignProperty } = useDesignSync({
    layer,
    onLayerUpdate,
    activeBreakpoint,
    activeUIState,
  });

  const fontSize = getDesignProperty('typography', 'fontSize') || '';

  // Hook automatically strips spaces
  const [fontSizeInput, setFontSizeInput] = useControlledInput(fontSize, extractMeasurementValue);

  const handleFontSizeChange = (value: string) => {
    setFontSizeInput(value); // Spaces never appear in input
    const sanitized = removeSpaces(value); // Extra safety layer
    updateDesignProperty('typography', 'fontSize', sanitized || null);
  };

  return (
    <Input
      value={fontSizeInput}
      onChange={(e) => handleFontSizeChange(e.target.value)}
    />
  );
}
```

### Why This Matters

```typescript
// ❌ WITHOUT sanitization:
User types: "10 rem"
Creates class: "text-[10 rem]"  // INVALID Tailwind!
Result: Broken styling, compilation errors

// ✅ WITH sanitization:
User types: "10 rem"
Sanitized to: "10rem"
Creates class: "text-[10rem]"  // VALID Tailwind!
Result: Perfect styling ✓
```

### Checklist for Design Property Inputs

- [ ] Import `removeSpaces` from `@/lib/utils`
- [ ] Use `useControlledInput` hook for state management
- [ ] Call `removeSpaces()` in change handlers
- [ ] Never create local `const sanitize =` helpers
- [ ] Test that spaces don't appear when typing

---

## 🌍 17. Environment Variables

### Client vs Server

```typescript
// ✅ Client-side (accessible in browser)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

// ✅ Server-side ONLY (API routes, Server Components)
const supabaseServiceKey = process.env.SUPABASE_SECRET_KEY;

// ❌ WRONG: Server-side var in client code
const secret = process.env.SECRET_KEY; // Undefined in browser!
```

### Rules
- Prefix with `NEXT_PUBLIC_` ONLY for client-side variables
- NEVER expose secrets/keys to the client
- Validate required env vars at startup
- Document all env vars in README or `.env.example`

---

## 🧪 18. TypeScript Standards

### NEVER Use `any`

```typescript
// ❌ FORBIDDEN
function handleData(data: any) {
  return data.map((item: any) => item.name);
}

// ✅ CORRECT: Proper types
interface Item {
  id: string;
  name: string;
}

function handleData(data: Item[]): string[] {
  return data.map(item => item.name);
}
```

### Use Proper Interfaces

```typescript
// ✅ Define clear interfaces
interface LayerProps {
  layer: Layer;
  selectedLayerId: string | null;
  onSelect: (id: string) => void;
}

const LayerItem: React.FC<LayerProps> = ({ layer, selectedLayerId, onSelect }) => {
  // Component implementation
};
```

### Type Inference

Let TypeScript infer when obvious:

```typescript
// ✅ Inference is clear
const count = 5; // number
const name = 'John'; // string
const items = pages.map(p => p.id); // string[]

// ✅ Explicit when needed
const [data, setData] = useState<Page | null>(null);
```

---

## ✅ 19. ESLint & Code Quality

### Always Run Before Committing

```bash
npm run lint        # Check for issues
npm run lint:fix    # Auto-fix issues
npm run type-check  # Verify TypeScript
```

### Key Rules Enforced

- **Indentation**: 2 spaces
- **Quotes**: Single quotes (template literals allowed)
- **JSX**: Max 2 props per line
- **Spacing**: Spaces in object braces `{ key: value }`
- **Self-closing**: Space before `/>` (`<Button />`)
- **Brackets**: Tag-aligned closing brackets

```typescript
// ✅ CORRECT formatting
const config = { name: 'YCode', version: '1.0' };

<Button
  variant="primary"
  size="lg"
  onClick={handleClick}
/>

// ❌ WRONG formatting
const config={name:'YCode',version:'1.0'};

<Button variant="primary" size="lg" onClick={handleClick}/>
```

---

## 🚫 Common Anti-Patterns to AVOID

### 1. Not Using ShadCN Components
```typescript
// ❌ Creating custom button
<button className="px-4 py-2 bg-blue-500">Click</button>

// ✅ Using ShadCN
<Button variant="default">Click</Button>
```

### 2. Direct Supabase in API Routes
```typescript
// ❌ Direct Supabase
const supabase = await getSupabaseAdmin();
await supabase.from('pages').select();

// ✅ Use repository
await getAllPages();
```

### 3. Nested Ternaries
```typescript
// ❌ Nested ternaries
return loading ? <Spinner /> : error ? <Error /> : <Content />;

// ✅ Early returns
if (loading) return <Spinner />;
if (error) return <Error />;
return <Content />;
```

### 4. Missing Loading States
```typescript
// ❌ No loading state
const [data, setData] = useState([]);
useEffect(() => { fetch().then(setData); }, []);
return <List data={data} />;

// ✅ With loading state
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
  fetch().then(setData).finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
return <List data={data} />;
```

### 5. Using `any` Type
```typescript
// ❌ Using any
function process(data: any) { }

// ✅ Proper types
function process(data: Page[]) { }
```

### 6. Deep Relative Imports
```typescript
// ❌ Deep relative paths
import { Button } from '../../../../components/ui/button';

// ✅ Use path alias
import { Button } from '@/components/ui/button';
```

### 7. Not Abstracting Repeated Logic
```typescript
// ❌ Repeated debounce logic everywhere
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
  const timer = setTimeout(() => setDebouncedValue(value), 500);
  return () => clearTimeout(timer);
}, [value]);

// ✅ Extract to custom hook
const debouncedValue = useDebounce(value, 500);
```

---

## 🎯 Quick Checklist Before Committing

- [ ] Using ShadCN components for all UI primitives?
- [ ] Extracted reusable logic into hooks/utils?
- [ ] Components are small and focused (single responsibility)?
- [ ] Used proper TypeScript types (no `any`)?
- [ ] Imports organized correctly?
- [ ] Using `@/` path aliases (not relative paths)?
- [ ] Loading states for all async operations?
- [ ] Error handling with try/catch/finally?
- [ ] Early returns instead of nested ternaries?
- [ ] Added `'use client'` only when needed?
- [ ] API routes use repository pattern?
- [ ] Consistent API response format?
- [ ] Event handlers named `handleX`?
- [ ] Boolean props named `isX`, `hasX`, `shouldX`?
- [ ] **Null check after `getSupabaseAdmin()` in ALL cases?**
- [ ] **New icons added to BOTH type definition AND ICONS object?**
- [ ] **Repository functions use dedicated `CreateXData` / `UpdateXData` interfaces?**
- [ ] **Zod schemas use `message` parameter (not `required_error`)?**
- [ ] Run `npm run lint:fix` and `npm run type-check`?
- [ ] Sanitize design property inputs with `removeSpaces`?
- [ ] Use `useControlledInput` for input state management?
- [ ] **Migrations are idempotent (safe to run multiple times)?**

---

## 🔧 20. Supabase Client Null Checking (CRITICAL)

### ALWAYS Check for Null After getSupabaseAdmin()

The `getSupabaseAdmin()` function can return `null` if Supabase is not configured. **ALWAYS** add a null check immediately after calling it.

```typescript
// ✅ CORRECT: Always check for null
export async function getPageById(id: string): Promise<Page | null> {
  const client = await getSupabaseAdmin();

  if (!client) {
    throw new Error('Supabase not configured');
  }

  const { data, error } = await client
    .from('pages')
    .select('*')
    .eq('id', id)
    .single();

  if (error && error.code !== 'PGRST116') {
    throw new Error(`Failed to fetch page: ${error.message}`);
  }

  return data;
}

// ❌ WRONG: No null check - TypeScript error
export async function getPageById(id: string): Promise<Page | null> {
  const client = await getSupabaseAdmin();

  // TypeScript error: 'client' is possibly 'null'
  const { data, error } = await client
    .from('pages')
    .select('*')
    .eq('id', id)
    .single();

  return data;
}
```

### In API Routes

```typescript
// ✅ CORRECT: Return error response if client is null
export async function GET(request: NextRequest) {
  try {
    const client = await getSupabaseAdmin();

    if (!client) {
      return NextResponse.json(
        { error: 'Supabase not configured' },
        { status: 500 }
      );
    }

    // Use client safely
    const { data } = await client.from('pages').select('*');
    return NextResponse.json({ data });
  } catch (error) {
    return NextResponse.json({ error: 'Server error' }, { status: 500 });
  }
}
```

### Rules
1. **ALWAYS** check `if (!client)` after `await getSupabaseAdmin()`
2. In repository functions: **throw an error** with descriptive message
3. In API routes: **return error response** with status 500
4. **NEVER** use the client without checking for null first

---

## 🎨 21. Icon Component Extension

### Adding New Icons

When you need to add a new icon to the `Icon` component, follow these steps:

**Step 1: Update the TypeScript Union Type**

```typescript
// components/ui/icon.tsx
export interface IconProps extends React.SVGProps<SVGSVGElement> {
  name: (
    'x' | 'layers' | 'database' | 'plus' | /* ...existing icons... */
    | 'pencil' | 'eye' | 'eye-off' | 'trash' | 'upload' // Add your new icons here
  );
}
```

**Step 2: Add SVG Definition to ICONS Object**

```typescript
// components/ui/icon.tsx
const ICONS: Record<IconProps['name'], React.ReactNode> = {
  // ...existing icons...

  pencil: (
    <>
      <path d="M8.85355339,0.146446609 C9.04881554..." />
    </>
  ),

  trash: (
    <>
      <path d="M4.5,1 L7.5,1 L7.5,2..." />
    </>
  ),

  // Add more icons here
};
```

### Rules
1. **ALWAYS** update both the type definition AND the ICONS object
2. Use **kebab-case** for icon names (e.g., `eye-off`, not `eyeOff`)
3. Get SVG paths from **Lucide icons** or **Heroicons** for consistency
4. Set `viewBox="0 0 12 12"` for all icons (they're scaled to 12x12)
5. Test the icon renders correctly before committing

### ❌ Common Mistake

```typescript
// ❌ WRONG: Added icon to ICONS but forgot to update type
<Icon name="new-icon" /> // TypeScript error: Type '"new-icon"' is not assignable
```

---

## 🗄️ 22. Repository Input Type Patterns

### Create Dedicated Input Interfaces for Repository Functions

Instead of using complex `Omit<>` types, create clear input interfaces for create/update operations.

```typescript
// ✅ CORRECT: Dedicated input interface
import type { Component, Layer } from '@/types';

export interface CreateComponentData {
  name: string;
  layers: Layer[];
}

export async function createComponent(
  componentData: CreateComponentData
): Promise<Component> {
  const client = await getSupabaseAdmin();

  if (!client) {
    throw new Error('Supabase not configured');
  }

  // The repository sets is_published, publish_key, etc.
  const { data, error } = await client
    .from('components')
    .insert({
      name: componentData.name,
      layers: componentData.layers,
      is_published: false,
      publish_key: generateKey(),
      // ...other auto-generated fields
    })
    .select()
    .single();

  if (error) throw new Error(`Failed to create: ${error.message}`);
  return data;
}

// ❌ WRONG: Using Omit includes fields that should be auto-set
export async function createComponent(
  componentData: Omit<Component, 'id' | 'created_at' | 'updated_at'>
): Promise<Component> {
  // This requires is_published and publish_key to be passed in,
  // but they should be set automatically!
}
```

### Pattern for Update Functions

```typescript
// ✅ CORRECT: Update interface with all optional fields
export interface UpdateComponentData {
  name?: string;
  layers?: Layer[];
  // Only fields that can actually be updated
}

export async function updateComponent(
  id: string,
  componentData: UpdateComponentData
): Promise<Component> {
  // Implementation
}
```

### Rules
1. **CREATE**: Define `CreateXData` interface with only required input fields
2. **UPDATE**: Define `UpdateXData` interface with all optional fields
3. **NEVER** use `Omit<FullType, ...>` for input types
4. Auto-generated fields (`id`, `created_at`, `is_published`, `publish_key`) should NOT be in input interfaces
5. The repository layer is responsible for setting auto-generated fields

---

## 🔍 23. Zod Validation Best Practices

### Use Correct Parameter Names

When defining Zod schemas, use the correct parameter names for your validation library version.

```typescript
// ✅ CORRECT: Use 'message' for error messages
const formSchema = z.object({
  name: z.string().min(2, {
    message: 'Name must be at least 2 characters.',
  }),
  type: z.enum(['text', 'number', 'boolean', 'date', 'reference'], {
    message: 'Please select a field type.',
  }),
});

// ❌ WRONG: Using 'required_error' (deprecated/invalid parameter)
const formSchema = z.object({
  type: z.enum(['text', 'number'], {
    required_error: 'Please select a type.', // TypeScript error
  }),
});
```

### Common Zod Patterns

```typescript
// String with constraints
z.string().min(1, { message: 'Required' }).max(100)

// Optional field
z.string().optional()

// Nullable field
z.string().nullable()

// Enum with message
z.enum(['draft', 'published'], { message: 'Invalid status' })

// Number with range
z.number().min(0).max(100)

// Email validation
z.string().email({ message: 'Invalid email' })

// Custom validation
z.string().refine((val) => val.startsWith('test'), {
  message: 'Must start with "test"',
})
```

---

## 🚀 Team Collaboration Best Practices

### For Fast-Moving Teams Using Cursor

1. **Think abstraction first** - Before coding, ask "Can I reuse something?"
2. **Use ShadCN religiously** - Never reinvent UI primitives
3. **Keep PRs small** - Small, focused changes prevent conflicts
4. **Follow naming conventions strictly** - Makes searching and refactoring easy
5. **Document complex logic** - Help your teammates understand WHY
6. **Run linters before committing** - Catch issues early
7. **Prefer composition over duplication** - DRY principle
8. **Use TypeScript properly** - Types are documentation

---

## 🗃️ 24. Database Migrations (CRITICAL for Templates)

### ALWAYS Write Idempotent Migrations

Migrations MUST be safe to run multiple times. This is critical because:
1. **Template System**: When applying templates, migrations run on freshly inserted template data
2. **Schema already exists**: The database schema is already up-to-date when template migrations run
3. **Data transformations**: Only the data parts actually execute on template data

### Schema Changes: Use IF EXISTS / IF NOT EXISTS

```typescript
// ✅ CORRECT: Idempotent schema changes
export async function up(knex: Knex): Promise<void> {
  // Check before adding column
  const hasColumn = await knex.schema.hasColumn('pages', 'new_field');
  if (!hasColumn) {
    await knex.schema.alterTable('pages', (table) => {
      table.string('new_field').defaultTo('');
    });
  }
}

// ✅ CORRECT: Raw SQL with IF NOT EXISTS
await knex.raw(`
  ALTER TABLE pages 
  ADD COLUMN IF NOT EXISTS new_field TEXT DEFAULT '';
`);

// ❌ WRONG: Will fail if column exists
await knex.schema.alterTable('pages', (table) => {
  table.string('new_field').defaultTo('');
});
```

### Data Transformations: Use Safe WHERE Clauses

```typescript
// ✅ CORRECT: Only updates rows that need it
await knex.raw(`
  UPDATE pages 
  SET slug = LOWER(REGEXP_REPLACE(name, '[^a-zA-Z0-9]+', '-', 'g'))
  WHERE slug IS NULL OR slug = '';
`);

// ❌ WRONG: Overwrites all data on every run
await knex('pages').update({ 
  slug: knex.raw("LOWER(REGEXP_REPLACE(name, '[^a-zA-Z0-9]+', '-', 'g'))") 
});
```

### Template-Affecting Tables

These tables are exported/imported with templates - migrations affecting them MUST be idempotent:
- `pages`, `page_folders`, `page_layers`
- `components`
- `collections`, `collection_fields`, `collection_items`, `collection_item_values`
- `assets`, `asset_folders`
- `layer_styles`
- `locales`, `translations`

### Migration Checklist

- [ ] Schema changes use `IF NOT EXISTS` or check `hasColumn()`
- [ ] Data updates use `WHERE` clauses that skip already-transformed rows
- [ ] New NOT NULL columns have DEFAULT values
- [ ] Migration can run twice without error
- [ ] Migration can run on fresh template data

### Full Example

```typescript
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
  // 1. Add column (idempotent)
  const hasColumn = await knex.schema.hasColumn('pages', 'seo_title');
  if (!hasColumn) {
    await knex.schema.alterTable('pages', (table) => {
      table.string('seo_title').defaultTo('');
    });
  }

  // 2. Transform data (idempotent - only empty values)
  await knex.raw(`
    UPDATE pages 
    SET seo_title = LEFT(name, 60)
    WHERE seo_title IS NULL OR seo_title = '';
  `);
}

export async function down(knex: Knex): Promise<void> {
  const hasColumn = await knex.schema.hasColumn('pages', 'seo_title');
  if (hasColumn) {
    await knex.schema.alterTable('pages', (table) => {
      table.dropColumn('seo_title');
    });
  }
}
```

---

**Remember**: Consistency > Cleverness. A fast-moving team needs predictable patterns, not creative solutions.
