---
name: "skillslap-component-builder"
description: "Generate Next.js 16 App Router components following SkillSlap patterns: Tailwind + Shadcn, server/client split, typed Supabase queries, no SELECT *. Describe the UI you need and get a production-ready component."
metadata:
  version: "1.0.0"
---

# SkillSlap Component Builder

> **Purpose:** Generate a production-ready Next.js 16 App Router component following SkillSlap's exact codebase conventions. The output should be paste-ready into `skillslap/components/` with no modification.

---

## Invocation

```
/component <description>
```

**Examples:**
- `/component A skill report modal — user selects a reason from a dropdown and submits`
- `/component A collections sidebar showing the user's saved skill collections with add/remove`
- `/component A skill version history timeline showing diffs between versions`

---

## SkillSlap Component Conventions

### Server vs Client

**Server component** (default — no `'use client'`):
- Fetches data directly from Supabase via `createClient()` from `@/lib/supabase/server`
- Cannot use hooks, event listeners, or browser APIs
- Use for: pages, data-fetching wrappers, layout sections

**Client component** (add `'use client'` at top):
- Required for: `useState`, `useEffect`, event handlers, `useRouter`, `useSearchParams`
- Keep client components small — push data fetching up to a server parent

```tsx
// Server parent passes data down
async function SkillCollectionsList({ userId }: { userId: string }) {
  const supabase = await createClient()
  const { data } = await (supabase.from('skill_collections') as any)
    .select('id, title, created_at')
    .eq('user_id', userId)
  return <SkillCollectionsClient collections={data ?? []} />
}
```

### Supabase queries — NEVER SELECT *
Always use explicit column lists. For `skills` table use `SKILLS_COLUMNS` from `@/lib/supabase/skills-columns`:
```tsx
import { SKILLS_COLUMNS } from '@/lib/supabase/skills-columns'

const { data } = await supabase
  .from('skills')
  .select(`${SKILLS_COLUMNS}, author:users!skills_author_id_fkey(id, username, avatar_url)`)
  .eq('status', 'active')
```

For joins or partial selects, add `as any` to satisfy the typed client:
```tsx
const { data } = await (supabase.from('skill_collections') as any)
  .select('id, title, skill_count')
```

### Styling — Tailwind + `cn()` utility
```tsx
import { cn } from '@/lib/utils/cn'

// Always use cn() for conditional classes
<div className={cn(
  'base-classes here',
  isActive && 'active-classes',
  className  // always accept className prop
)} />
```

**Color palette:**
- Background: `bg-gray-950`, `bg-gray-900`, `bg-gray-800`
- Borders: `border-gray-800`, `border-gray-700`
- Text: `text-white`, `text-gray-300`, `text-gray-400`, `text-gray-500`
- Accent: `text-purple-400`, `border-purple-600/50`, `bg-purple-600`
- Success: `text-green-400`
- Warning: `text-yellow-400`
- Danger: `text-red-400`

### Shadcn UI components (already installed)
```tsx
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import * as Dialog from '@radix-ui/react-dialog'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
```

### Icons — lucide-react only
```tsx
import { Plus, X, Search, ChevronDown, Loader2 } from 'lucide-react'
```

### Error + loading states
```tsx
// Loading skeleton
<div className="h-8 bg-gray-800 rounded animate-pulse" />

// Error display
<p className="text-sm text-red-400">{error}</p>

// Empty state
<div className="text-center py-12 text-gray-500">
  <p className="text-sm">No items yet</p>
</div>
```

### Forms + mutations (client component)
```tsx
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'  // note: client, not server

async function handleSubmit(e: React.FormEvent) {
  e.preventDefault()
  setLoading(true)
  setError(null)
  try {
    const supabase = createClient()
    const { error } = await supabase.from('table').insert({ ... })
    if (error) throw error
    onSuccess?.()
  } catch (err: any) {
    setError(err.message ?? 'Something went wrong')
  } finally {
    setLoading(false)
  }
}
```

### Props interface pattern
```tsx
interface MyComponentProps {
  // required props first
  skillId: string
  // optional props with defaults
  className?: string
  onSuccess?: () => void
}

export function MyComponent({ skillId, className, onSuccess }: MyComponentProps) {
```

### TypeScript types
```tsx
import type { SkillWithAuthor } from '@/lib/supabase/types'
```

---

## Output Format

1. **Main component file** — full TypeScript, no placeholders, no TODOs
2. **File path** — where it belongs in `skillslap/components/`
3. **Imports** — only what's used
4. **If server + client split needed** — output both files

---

## Checklist Before Outputting

- [ ] No `SELECT *` — explicit columns everywhere
- [ ] `as any` on Supabase joins
- [ ] `cn()` for all conditional classNames
- [ ] `className?: string` prop on every component
- [ ] Loading and error states present
- [ ] `'use client'` only where actually needed
- [ ] No hardcoded colors — use SkillSlap palette
