Back to catalog

frontend

Universal React/Next.js frontend expert. Handles shadcn/ui components, Tailwind CSS, Server/Client Components, anti-duplication checks. Auto-activates on keywords "frontend", "UI", "component", "page", "shadcn", "React", "Next.js" or paths app/**, components/**.

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: frontend
description: Universal React/Next.js frontend expert. Handles shadcn/ui components, Tailwind CSS, Server/Client Components, anti-duplication checks. Auto-activates on keywords "frontend", "UI", "component", "page", "shadcn", "React", "Next.js" or paths app/**, components/**.
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
---

# Frontend Development Skill

> **Universal React/Next.js Frontend Expert**
>
> Inspiré de : Vercel DX Principles, shadcn/ui Philosophy, Tailwind Best Practices

---

## Scope & Activation

**Chargé par:** EXECUTOR agent

**Auto-activé si keywords:**
- `frontend`, `UI`, `composant`, `page`, `dashboard`
- `React`, `Next.js`, `shadcn`, `Tailwind`
- `button`, `dialog`, `form`, `layout`
- Paths: `app/**`, `components/**`, `*.tsx`, `*.css`

**Frameworks supportés:**
- Next.js 14+ (App Router)
- React 18+ (avec Server/Client Components)
- shadcn/ui (Radix + Tailwind)
- Tailwind CSS 3+

---

## Auto-Détection Base Frontend (OBLIGATOIRE Phase 0)

**AVANT toute action, vérifier si base frontend existe dans projet:**

### Workflow Détection (STRICT)

```
1. Check si projet est NOUVEAU (pas de components/)
   → SI nouveau: Clone depuis BUILDER/.stack/

2. Check si components.json existe
   → SI absent: Clone depuis BUILDER/.stack/
   → SI présent: Read components.json

3. Read components.json → Récupère aliases
   → Vérifier: aliases.ui = "@/components/ui"
   → Vérifier: aliases.components = "@/components"

4. Glob components/ui/*.tsx
   → Liste tous composants shadcn disponibles
   → Update mental map: "[X] composants shadcn disponibles"

5. SI components/ui/ vide ou absent
   → Clone depuis BUILDER/.stack/
```

**Path base frontend (relatif):**
```
BUILDER/.stack/
```

**Localisation base:**
- Chercher dossier BUILDER/ en remontant arborescence
- Ou utiliser variable env si configurée
- Path par défaut: `../../BUILDER/.stack/` (depuis projet)

**Contenu base (à cloner):**
- `components/ui/` - 57 composants shadcn
- `app/globals.css` + `app/themes.css`
- `lib/utils.ts` + `lib/compose-refs.ts`
- `components.json` (config shadcn)
- `tsconfig.json`, `next.config.ts`, `postcss.config.mjs`
- `package.json` (dependencies optimisées)

**Commande clone (EXACTE - copy-paste ready):**

```bash
# ÉTAPE 1: Trouver BUILDER/.stack/ (méthode fiable)
# Cherche dans home directory
BUILDER_DIR=$(find ~ -maxdepth 3 -type d -name "BUILDER" 2>/dev/null | head -1)

# Si pas trouvé dans home, cherche partout (plus lent)
if [ -z "$BUILDER_DIR" ]; then
  BUILDER_DIR=$(find /home -maxdepth 4 -type d -name "BUILDER" 2>/dev/null | head -1)
fi

# Path .stack/
BUILDER_STACK="$BUILDER_DIR/.stack"

# ÉTAPE 2: Vérifier .stack/ existe
if [ ! -d "$BUILDER_STACK" ]; then
  echo "❌ BUILDER/.stack/ non trouvé"
  echo ""
  echo "Clone BUILDER repo d'abord:"
  echo "  cd ~"
  echo "  git clone https://github.com/nera0875/BUILDER.git"
  echo ""
  echo "Puis relance la commande"
  exit 1
fi

# ÉTAPE 3: Clone base frontend
echo "📦 Clone base frontend depuis $BUILDER_STACK"

# Copy tous les fichiers ET dossiers (y compris hidden)
cp -r "$BUILDER_STACK"/* . 2>/dev/null
cp -r "$BUILDER_STACK"/.[!.]* . 2>/dev/null

# ÉTAPE 4: Install dependencies
echo "📥 Installation dependencies..."
npm install

echo "✅ Base frontend clonée (57 composants shadcn ready)"
```

**Alternative si find échoue (fallback manuel):**

```bash
# Si commande automatique échoue, donner instructions user:
echo "❌ Détection automatique échouée"
echo ""
echo "Clone manuellement:"
echo ""
echo "1. Localise BUILDER repo:"
echo "   cd ~/BUILDER  # ou ton path"
echo ""
echo "2. Copy .stack/ vers projet:"
echo "   cp -r ~/.../BUILDER/.stack/* /ton/projet/"
echo ""
echo "3. Install dependencies:"
echo "   cd /ton/projet && npm install"
```

**Principe:** find limité à 3-4 depth max (performance). Si projet BUILDER bien placé (~/BUILDER ou ~/tools/BUILDER), détection auto fonctionne.

**Exemple détection:**
```json
// components.json trouvé
{
  "aliases": {
    "ui": "@/components/ui",
    "components": "@/components"
  }
}
```
→ **Conclusion:** Kit shadcn installé, vérifier components/ui/

**Impact anti-duplication:**
- ❌ Ne JAMAIS recréer `components/ui/button.tsx` si existe
- ❌ Ne JAMAIS run `npx shadcn init` si components.json existe
- ❌ Ne JAMAIS run `npx shadcn add button` si components/ui/button.tsx existe
- ✅ Importer depuis `@/components/ui/button`
- ✅ Créer composants custom dans `components/features/` uniquement

---

## ❌ ZONES INTERDITES (READ-ONLY - JAMAIS TOUCHER)

### components/ui/ - KIT SHADCN (STRICTEMENT INTERDIT)

**RÈGLES ABSOLUES:**
- ❌ Créer nouveau fichier dans `components/ui/`
- ❌ Modifier fichiers existants `components/ui/*.tsx`
- ❌ Supprimer composants shadcn
- ❌ Renommer fichiers `ui/`
- ❌ Ajouter props custom aux composants ui/
- ❌ Refactor code dans `ui/`

**SEULE ACTION AUTORISÉE:**
- ✅ **Importer uniquement** depuis `@/components/ui/[composant]`

```tsx
// ✅ AUTORISÉ
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle } from "@/components/ui/card";

// ❌ INTERDIT
// Modifier components/ui/button.tsx
// Créer components/ui/custom-button.tsx
```

**Raison:** `components/ui/` = Lego blocks de base shadcn. **READ-ONLY STRICT.** Customs dans `features/`.

---

### app/globals.css - STYLES GLOBAUX (1 SEUL FICHIER)

**INTERDIT:**
- ❌ Créer nouveaux fichiers `.css` (styles.css, custom.css, etc)
- ❌ CSS modules (`.module.css`)
- ❌ Styled-components
- ❌ CSS-in-JS (emotion, etc)
- ❌ Modifier directives Tailwind existantes (`@tailwind base`, etc)
- ❌ Supprimer CSS variables shadcn (`:root`, `.dark`)

**AUTORISÉ UNIQUEMENT:**
- ✅ Ajouter `@layer utilities` dans `globals.css` existant (si absolument nécessaire)

```css
/* ✅ AUTORISÉ dans globals.css existant */
@layer utilities {
  .custom-scrollbar::-webkit-scrollbar {
    width: 8px;
  }
}

/* ❌ INTERDIT - Pas de nouveau fichier CSS */
/* Pas de custom.css, styles.css, etc */
```

**Raison:** 1 seul point styling global (Tailwind). Fragmentation = chaos maintenance.

---

### app/themes.css - THÈMES SHADCN (READ-ONLY)

**INTERDIT:**
- ❌ Modifier variables CSS existantes
- ❌ Supprimer thèmes
- ❌ Ajouter nouveaux thèmes (sauf demande explicite user)

**Raison:** Thèmes shadcn pré-configurés. Modification = casse dark mode.

---

### lib/utils.ts - CN HELPER (READ-ONLY)

**INTERDIT:**
- ❌ Modifier fonction `cn()` (helper shadcn)
- ❌ Supprimer imports `clsx` ou `tailwind-merge`

**AUTORISÉ:**
- ✅ Créer nouveaux helpers dans fichiers séparés: `lib/[custom].ts`

```tsx
// ❌ INTERDIT - Modifier lib/utils.ts
export function cn(...) { /* NE PAS TOUCHER */ }

// ✅ AUTORISÉ - Nouveau fichier
// lib/date-helpers.ts
export function formatDate(date: Date) {
  return date.toLocaleDateString();
}
```

**Raison:** `cn()` = core shadcn helper. Modification = casse tous composants.

---

### Fichiers Config (READ-ONLY sauf cas spécifiques)

**INTERDIT de modifier sans raison:**
- ❌ `components.json` (config shadcn)
- ❌ `tsconfig.json` (TypeScript paths)
- ❌ `next.config.ts` (Next.js config)
- ❌ `postcss.config.mjs` (PostCSS config)
- ❌ `tailwind.config.ts` (si existe - Tailwind v3)

**AUTORISÉ seulement si:**
- ✅ Feature nécessite nouvelle lib → Update `next.config.ts`
- ✅ Nouveau alias path → Update `tsconfig.json` paths
- ✅ Nouveau plugin Tailwind → Update `tailwind.config.ts`

**Principe:** Config établie = stable. Modification = risque régression.

---

## ✅ OÙ CRÉER NOUVEAUX COMPOSANTS/FICHIERS

### components/features/[feature]/ - COMPOSANTS CUSTOM

**Structure recommandée:**

```
components/
├── ui/                        ← READ-ONLY (shadcn)
│   └── button.tsx
├── layout/                    ← CRÉER ICI (layouts app)
│   ├── sidebar.tsx
│   ├── header.tsx
│   └── footer.tsx
└── features/                  ← CRÉER ICI (composants métier)
    ├── dashboard/
    │   ├── stats-card.tsx     ← Custom composant (utilise ui/card)
    │   ├── chart.tsx
    │   └── filters.tsx
    ├── kanban/
    │   ├── board.tsx
    │   ├── column.tsx
    │   └── task-card.tsx      ← Custom composant (utilise ui/card)
    └── pomodoro/
        └── timer.tsx
```

**Exemple composant custom:**

```tsx
// components/features/dashboard/stats-card.tsx
"use client";

import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { TrendingUp } from "lucide-react";

export function StatsCard({ title, value, trend }) {
  return (
    <Card className="border-2 border-primary/20 hover:shadow-lg transition">
      <CardHeader className="flex flex-row items-center justify-between">
        <CardTitle className="text-sm font-medium">{title}</CardTitle>
        <TrendingUp className="h-4 w-4 text-primary" />
      </CardHeader>
      <CardContent>
        <p className="text-3xl font-bold">{value}</p>
        <p className="text-xs text-muted-foreground">
          {trend > 0 ? "+" : ""}{trend}% from last month
        </p>
      </CardContent>
    </Card>
  );
}
```

**Principe:** Compose avec `ui/`, custom styling via Tailwind className, logique métier séparée.

---

### components/layout/ - LAYOUTS APP

**Créer ici:**
- Sidebar navigation
- Header/Navbar
- Footer
- AppShell/DashboardLayout

**Exemple:**

```tsx
// components/layout/sidebar.tsx
"use client";

import { Button } from "@/components/ui/button";
import { Home, Settings, User } from "lucide-react";

export function Sidebar() {
  return (
    <aside className="h-full border-r bg-card">
      <nav className="flex flex-col gap-2 p-4">
        <Button variant="ghost" className="justify-start">
          <Home className="mr-2 h-4 w-4" />
          Dashboard
        </Button>
        <Button variant="ghost" className="justify-start">
          <User className="mr-2 h-4 w-4" />
          Profile
        </Button>
        <Button variant="ghost" className="justify-start">
          <Settings className="mr-2 h-4 w-4" />
          Settings
        </Button>
      </nav>
    </aside>
  );
}
```

---

### app/[route]/ - PAGES NEXT.JS

**Structure app router:**

```
app/
├── globals.css              ← READ-ONLY (sauf @layer utilities)
├── themes.css               ← READ-ONLY
├── layout.tsx               ← CRÉER (root layout)
├── page.tsx                 ← CRÉER (home page)
├── dashboard/
│   ├── layout.tsx           ← CRÉER (dashboard layout avec sidebar)
│   ├── page.tsx             ← CRÉER (dashboard home)
│   └── settings/
│       └── page.tsx         ← CRÉER (nested route)
└── (auth)/                  ← CRÉER (route group)
    ├── login/
    │   └── page.tsx
    └── register/
        └── page.tsx
```

**Exemple page:**

```tsx
// app/dashboard/page.tsx (Server Component)
import { prisma } from "@/lib/prisma";
import { StatsCard } from "@/components/features/dashboard/stats-card";

export default async function DashboardPage() {
  const stats = await prisma.task.groupBy({
    by: ['status'],
    _count: true
  });

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-4 p-6">
      {stats.map(stat => (
        <StatsCard
          key={stat.status}
          title={stat.status}
          value={stat._count}
          trend={12}
        />
      ))}
    </div>
  );
}
```

---

### lib/ - UTILITIES CUSTOM

**Créer nouveaux helpers:**

```
lib/
├── utils.ts           ← READ-ONLY (cn helper shadcn)
├── date-helpers.ts    ← CRÉER (custom date utils)
├── api-client.ts      ← CRÉER (fetch wrapper)
├── validators.ts      ← CRÉER (Zod schemas)
└── constants.ts       ← CRÉER (app constants)
```

**Exemple:**

```tsx
// lib/date-helpers.ts
import { format } from "date-fns";

export function formatDate(date: Date): string {
  return format(date, "MMM dd, yyyy");
}

export function formatDateTime(date: Date): string {
  return format(date, "MMM dd, yyyy HH:mm");
}
```

---

### hooks/ - CUSTOM REACT HOOKS

**Créer hooks custom:**

```
hooks/
├── use-local-storage.ts
├── use-media-query.ts
└── use-debounce.ts
```

**Exemple:**

```tsx
// hooks/use-local-storage.ts
"use client";

import { useState, useEffect } from "react";

export function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(initialValue);

  useEffect(() => {
    const item = window.localStorage.getItem(key);
    if (item) setStoredValue(JSON.parse(item));
  }, [key]);

  const setValue = (value: T) => {
    setStoredValue(value);
    window.localStorage.setItem(key, JSON.stringify(value));
  };

  return [storedValue, setValue] as const;
}
```

---

## Workflow Création Composant (STRICT - Phase par Phase)

### Phase 0: Vérification Base Clonée

**AVANT toute création composant:**

```bash
# Vérifier base frontend clonée depuis BUILDER/.stack/
ls components/ui/ && echo "✅ Base OK" || echo "❌ CLONE BASE FIRST"
```

**SI base absente:** Clone BUILDER/.stack/ (voir section Auto-Détection)

---

### Phase 1: Anti-Duplication Check (OBLIGATOIRE)

**⚡ OPTIMIZED: Grep RAG-Style (1-2 tool uses, 2s)**

**1. ✅ Check si composant shadcn existe dans `components/ui/`**
```bash
Grep "export.*Button" components/ui/ --output files_with_matches
→ components/ui/button.tsx (existe) → Réutilise

Grep "export.*Card" components/ui/ --output files_with_matches
→ components/ui/card.tsx (existe) → Réutilise
```

**Rappel:** BUILDER/.stack/ contient **57 composants shadcn**.
Ne JAMAIS recréer si existe.

**2. ✅ Check si composant custom existe dans `components/features/`**
```bash
Grep "export (default )?(function |const )?StatsCard" components/features/ -i --output files_with_matches
→ Si trouvé: components/features/dashboard/stats-card.tsx → Réutilise
→ Si vide: Composant existe pas → Créer

Grep "export.*TaskCard" components/features/ -i --output files_with_matches
→ Check existence (1 tool use, 2s)
```

**❌ PAS ÇA (lent - 30+ tools):**
```bash
ls components/ui/*.tsx → Liste 57 fichiers
Read button.tsx, Read card.tsx, Read dialog.tsx...
→ 30+ tool uses, 25s
```

**✅ ÇA (rapide - 1-2 tools):**
```bash
Grep "export.*[ComponentName]" components/ --output files_with_matches
→ 1 tool use, 2s
```

---

### Phase 2: Décision Emplacement

**3. ✅ Décide emplacement selon type:**

| Type composant | Emplacement | Exemple |
|----------------|-------------|---------|
| Layout app | `components/layout/` | `sidebar.tsx`, `header.tsx` |
| Composant métier | `components/features/[feature]/` | `stats-card.tsx`, `task-card.tsx` |
| Page Next.js | `app/[route]/page.tsx` | `app/dashboard/page.tsx` |
| Hook custom | `hooks/` | `use-local-storage.ts` |
| Util custom | `lib/` | `date-helpers.ts` |

---

### Phase 3: Composition avec Base

**4. ✅ Compose avec shadcn ui/ (base)**
- Import depuis `@/components/ui/[composant]`
- Styling via Tailwind className uniquement
- Logique métier custom dans composant

```tsx
// ✅ CORRECT - Composition
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";

export function StatsCard({ title, value }) {
  return (
    <Card className="border-2 border-primary/20">  {/* Custom styling */}
      <CardHeader>
        <CardTitle>{title}</CardTitle>
        <p className="text-3xl font-bold">{value}</p>
      </CardHeader>
    </Card>
  );
}
```

---

### Phase 4: Server vs Client

**5. ✅ "use client" si nécessaire (CHECKLIST)**

**OBLIGATOIRE "use client" SI:**
- ✅ Hooks React (`useState`, `useEffect`, `useContext`)
- ✅ Event handlers (`onClick`, `onChange`, `onSubmit`)
- ✅ Browser APIs (`window`, `document`, `localStorage`)
- ✅ Third-party libs interactives (react-hook-form)

**PAS "use client" SI:**
- ✅ Fetch data serveur (Prisma, fetch API)
- ✅ Pas d'interactivité (display statique)
- ✅ SEO important

```tsx
// ✅ Client Component (hooks + events)
"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";

export function Counter() {
  const [count, setCount] = useState(0);
  return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}
```

```tsx
// ✅ Server Component (fetch data)
import { prisma } from "@/lib/prisma";

export default async function TasksPage() {
  const tasks = await prisma.task.findMany();
  return <div>{tasks.map(t => <div key={t.id}>{t.name}</div>)}</div>;
}
```

---

### Pattern création composant custom

```tsx
// components/features/kanban/task-card.tsx
"use client";

import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Trash, Edit } from "lucide-react";

interface TaskCardProps {
  task: {
    id: string;
    title: string;
    priority: "low" | "medium" | "high";
  };
  onEdit: (id: string) => void;
  onDelete: (id: string) => void;
}

export function TaskCard({ task, onEdit, onDelete }: TaskCardProps) {
  return (
    <Card className="group hover:shadow-md transition cursor-pointer">
      <CardHeader className="flex flex-row items-start justify-between">
        <div className="flex-1">
          <CardTitle className="text-base">{task.title}</CardTitle>
          <Badge
            variant={task.priority === "high" ? "destructive" : "secondary"}
            className="mt-2"
          >
            {task.priority}
          </Badge>
        </div>
        <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition">
          <Button
            variant="ghost"
            size="icon"
            onClick={() => onEdit(task.id)}
          >
            <Edit className="h-4 w-4" />
          </Button>
          <Button
            variant="ghost"
            size="icon"
            onClick={() => onDelete(task.id)}
          >
            <Trash className="h-4 w-4 text-destructive" />
          </Button>
        </div>
      </CardHeader>
    </Card>
  );
}
```

**Checklist:**
- ✅ Imports depuis `@/components/ui/`
- ✅ "use client" (car onClick events)
- ✅ TypeScript interface
- ✅ Styling Tailwind uniquement
- ✅ Composition (Card + Button + Badge depuis ui/)
- ✅ Props typées
- ✅ Logique métier (onEdit, onDelete callbacks)

---

## Architecture Stricte (Non-Negotiable)

### tsconfig.json paths (OBLIGATOIRE)

```json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

**OU** si pas de `/src`:

```json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}
```

---

### Structure Projet Standard (après clone BUILDER/.stack/)

**Structure EXACTE après clone base:**

```
projet/
├── .gitignore                    ← DE BASE
├── README.md                     ← PEUT modifier (doc projet)
├── package.json                  ← DE BASE (modifiable npm install)
├── tsconfig.json                 ← DE BASE (READ-ONLY sauf alias)
├── next.config.ts                ← DE BASE (READ-ONLY sauf feature)
├── postcss.config.mjs            ← DE BASE (READ-ONLY)
├── components.json               ← DE BASE (READ-ONLY)
│
├── app/
│   ├── globals.css               ← DE BASE (READ-ONLY sauf @layer)
│   ├── themes.css                ← DE BASE (READ-ONLY)
│   ├── layout.tsx                ← CRÉER (root layout)
│   ├── page.tsx                  ← CRÉER (home page)
│   ├── dashboard/                ← CRÉER (feature routes)
│   │   ├── layout.tsx
│   │   └── page.tsx
│   └── (auth)/                   ← CRÉER (route groups)
│       ├── login/
│       └── register/
│
├── components/
│   ├── ui/                       ← DE BASE (57 composants READ-ONLY)
│   │   ├── accordion.tsx
│   │   ├── button.tsx
│   │   ├── card.tsx
│   │   ├── dialog.tsx
│   │   └── ... (53 autres)
│   ├── layout/                   ← CRÉER (layouts app)
│   │   ├── sidebar.tsx
│   │   └── header.tsx
│   └── features/                 ← CRÉER (composants métier)
│       ├── dashboard/
│       │   ├── stats-card.tsx
│       │   └── chart.tsx
│       └── kanban/
│           └── board.tsx
│
├── lib/
│   ├── utils.ts                  ← DE BASE (READ-ONLY)
│   ├── compose-refs.ts           ← DE BASE (READ-ONLY)
│   ├── prisma.ts