Claude Code Plugins

Community-maintained marketplace

Feedback

Atomic Component Generator

@RomualdP/hoki
0
0

Génère des composants React suivant l'Atomic Design avec décomposition maximale et pattern Smart/Dumb. MANDATORY pour tous composants. À utiliser lors de la création de composants, pages, ou quand l'utilisateur mentionne "component", "atomic", "smart", "dumb", "decompose", "React", "UI".

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name Atomic Component Generator
description Génère des composants React suivant l'Atomic Design avec décomposition maximale et pattern Smart/Dumb. MANDATORY pour tous composants. À utiliser lors de la création de composants, pages, ou quand l'utilisateur mentionne "component", "atomic", "smart", "dumb", "decompose", "React", "UI".
allowed-tools Read, Write, Edit, Glob, Grep

Atomic Component Generator

🎯 Mission

Créer des composants React maximalement décomposés suivant l'Atomic Design et le pattern Smart/Dumb pour une réutilisabilité et maintenabilité maximales.

⚛️ Philosophie Atomic Design

Le Principe

Atomic Design décompose l'UI en 5 niveaux :

  1. Atoms (Atomes) : Plus petites unités (Button, Input, Label)
  2. Molecules (Molécules) : Groupes d'atomes (FormField = Label + Input + Error)
  3. Organisms (Organismes) : Groupes de molécules (Header = Logo + Nav + UserMenu)
  4. Templates : Layouts avec placeholders
  5. Pages : Templates avec données réelles

Dans ce projet, nous utilisons principalement 3 niveaux :

  • Atoms : Composants de base (components/ui/ - shadcn/ui)
  • Smart Components : Logique + état (features/*/components/*Form, *List)
  • Dumb Components : Présentation pure (features/*/components/*Step, *Card)

Règle d'Or : Décomposition Maximale

CRITICAL : Components MUST be maximally decomposed.

Pourquoi :

  • Réutilisabilité : Petits composants = réutilisables partout
  • Testabilité : Petits composants = faciles à tester
  • Maintenabilité : Un composant = Une responsabilité
  • Lisibilité : Code clair et compréhensible
  • Collaboration : Équipe peut travailler en parallèle

Mauvais signe :

  • ❌ Fichier > 150 lignes
  • ❌ Composant fait plusieurs choses
  • ❌ JSX imbriqué sur 10+ niveaux
  • ❌ Logique + présentation mélangées

🧩 Pattern Server/Client Components (Next.js 16)

🎯 Server Components par défaut

IMPORTANT : Depuis Next.js 16, TOUS les composants sont Server Components par défaut.

Server Components :

  • ✅ Fetch data server-side (async/await)
  • ✅ Accès direct à la base de données / APIs backend
  • ✅ Zero JavaScript client-side pour le data fetching
  • ✅ Meilleur SEO (contenu pré-rendu)
  • ✅ Pas besoin de "use client"

Quand utiliser :

  • Pages, layouts, templates
  • Widgets qui affichent des données fetchées
  • Composants sans interactivité

Exemple :

// Server Component (par défaut, pas de "use client")
export async function TeamsWidget() {
  const teams = await getTeams(); // Fetch server-side

  return <div>{teams.map(t => <TeamCard key={t.id} team={t} />)}</div>;
}

🖱️ Client Components (uniquement si nécessaire)

Client Components (avec "use client") :

  • ✅ Interactivité (onClick, onChange, etc.)
  • ✅ Hooks React (useState, useEffect, useContext)
  • ✅ Stores Zustand (state client)
  • ✅ Browser APIs (localStorage, etc.)

Quand utiliser :

  • Formulaires interactifs
  • Composants avec state UI (modals, dropdowns)
  • Event handlers
  • useOptimistic, useTransition

Exemple :

"use client"; // Requis pour Client Component

export function TeamForm() {
  const [name, setName] = useState("");

  return <input value={name} onChange={(e) => setName(e.target.value)} />;
}

📦 Pattern de Composition Server → Client

Règle d'or : Server Component parent → Client Components enfants

// ✅ BON - Server Component avec fetch
export async function TeamsList() {
  const teams = await getTeams(); // Server-side

  return (
    <div>
      {teams.map(team => (
        <TeamCard key={team.id} team={team} /> // Client si interactivité
      ))}
    </div>
  );
}

// Client Component si besoin d'interactivité
"use client";
export function TeamCard({ team }: Props) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div onClick={() => setIsExpanded(!isExpanded)}>
      {/* ... */}
    </div>
  );
}

⚠️ CRITICAL: Component Folder Architecture

❌ NEVER Create Components in app/

RÈGLE ABSOLUE : Le dossier app/ est uniquement pour le routing Next.js.

❌ MAUVAIS (NEVER DO THIS):
app/
  └── players/
      ├── page.tsx
      └── components/          ← ❌ JAMAIS DE COMPOSANTS ICI
          ├── PlayerCard.tsx   ← ❌ MAUVAIS
          ├── PlayersList.tsx  ← ❌ MAUVAIS
          └── ...

✅ BON:
app/
  └── players/
      └── page.tsx             ← Routing only (max 50 lignes)

features/
  └── players/                 ← ✅ TOUS les composants vont ici
      ├── components/
      │   ├── PlayerCard.tsx   ← ✅ BON
      │   ├── PlayersList.tsx  ← ✅ BON
      │   └── ...
      ├── api/
      │   └── players.server.ts
      └── types/

Pourquoi Cette Règle Est Critique

  1. Séparation des responsabilités

    • app/ = Routing Next.js (infrastructure)
    • features/ = Business logic (métier)
  2. Réutilisabilité

    • Composants dans features/ peuvent être utilisés dans plusieurs routes
    • Composants dans app/ sont isolés à une seule route
  3. Maintenabilité

    • Structure claire et prévisible
    • Évite la duplication de code
  4. Best Practices Next.js 16

    • Pages dans app/ doivent être minces (orchestration)
    • Logique métier dans features/

Examples Réels

// ❌ MAUVAIS
// app/players/components/PlayerCard.tsx
export function PlayerCard({ player }) { ... }

// ❌ MAUVAIS
// app/teams/[id]/components/TeamDetails.tsx
export function TeamDetails({ team }) { ... }

// ✅ BON
// features/players/components/PlayerCard.tsx
export function PlayerCard({ player }) { ... }

// ✅ BON
// features/teams/components/TeamDetails.tsx
export function TeamDetails({ team }) { ... }

Page Structure

// app/players/page.tsx (Max 50 lignes - Orchestration uniquement)
import { PlayersListServer } from "@/features/players/components/PlayersListServer";
import { PlayersStatsGrid } from "@/features/players/components/PlayersStatsGrid";

export default async function PlayersPage() {
  return (
    <div>
      <Suspense fallback={<StatsGridSkeleton />}>
        <PlayersStatsGrid />
      </Suspense>

      <Suspense fallback={<ListSkeleton />}>
        <PlayersListServer />
      </Suspense>
    </div>
  );
}

🧩 Pattern Smart/Dumb Components (pour Client Components)

Smart Components (Container Components)

Responsabilités :

  • ✅ Gèrent l'état client (useState, Zustand stores)
  • ✅ Gèrent la logique UI
  • ✅ Appellent les Server Actions (mutations)
  • ✅ Orchestrent les Dumb Components
  • ✅ Gèrent les side effects (useEffect pour polling, refetch, etc.)

Caractéristiques :

  • "use client" directive
  • Nom se termine par Form, Manager, Container
  • Peu ou pas de JSX (délègue aux Dumb Components)
  • Beaucoup de logique JavaScript
  • Props minimales (reçoit peu, délègue beaucoup)

Emplacement : features/*/components/ (ex: ClubCreationForm, TeamManager)

Dumb Components (Presentational Components)

Responsabilités :

  • ✅ Affichent l'UI (JSX uniquement)
  • ✅ Reçoivent des props
  • ✅ Émettent des events (callbacks)
  • ✅ Aucun état (ou état UI minimal: hover, focus)
  • ✅ Aucune logique métier

Caractéristiques :

  • Nom descriptif : Step, Card, Item, Display, Section
  • Beaucoup de JSX
  • Peu ou pas de logique JavaScript
  • Props strictement typées (interfaces)
  • Pure functions (même props = même output)

Emplacement : features/*/components/ ou components/shared/ si réutilisable

Exemple de Séparation

// ❌ MAUVAIS - Tout dans un seul composant (150+ lignes)
export function ClubCreation() {
  const [step, setStep] = useState(1);
  const [clubData, setClubData] = useState({});
  const [isPending, startTransition] = useTransition();

  const handleSubmit = async () => {
    startTransition(async () => {
      await createClubAction(clubData);
    });
  };

  return (
    <div>
      {step === 1 && (
        <div>
          <h2>Informations du club</h2>
          <input onChange={(e) => setClubData({...clubData, name: e.target.value})} />
          {/* 50 lignes de formulaire */}
        </div>
      )}
      {step === 2 && (
        <div>
          <h2>Choisir un plan</h2>
          {/* 50 lignes de sélection de plan */}
        </div>
      )}
      {/* ... */}
    </div>
  );
}

// ✅ BON - Décomposé en Smart + Dumb

// Smart Component (orchestration)
export function ClubCreationForm() {
  const [step, setStep] = useState(1);
  const { clubData, updateClubData } = useClubCreationStore();
  const [isPending, startTransition] = useTransition();

  const handleSubmit = async () => {
    startTransition(async () => {
      await createClubAction(clubData);
    });
  };

  return (
    <FormWizard currentStep={step} onStepChange={setStep}>
      {step === 1 && (
        <ClubInfoStep
          data={clubData}
          onChange={updateClubData}
          onNext={() => setStep(2)}
        />
      )}
      {step === 2 && (
        <PlanSelectionStep
          selectedPlan={clubData.plan}
          onSelectPlan={(plan) => updateClubData({ plan })}
          onNext={handleSubmit}
          isPending={isPending}
        />
      )}
    </FormWizard>
  );
}

// Dumb Components (présentation)
interface ClubInfoStepProps {
  data: Partial<ClubData>;
  onChange: (data: Partial<ClubData>) => void;
  onNext: () => void;
}

export function ClubInfoStep({ data, onChange, onNext }: ClubInfoStepProps) {
  return (
    <div className="space-y-4">
      <h2 className="text-2xl font-bold">Informations du club</h2>
      <FormField
        label="Nom du club"
        value={data.name}
        onChange={(name) => onChange({ name })}
      />
      <FormField
        label="Description"
        value={data.description}
        onChange={(description) => onChange({ description })}
      />
      <Button onClick={onNext}>Suivant</Button>
    </div>
  );
}

📏 Règles de Décomposition

1. Limite de Lignes

  • Page : MAX 50 lignes (composition uniquement)
  • Smart Component : MAX 100 lignes (logique + orchestration)
  • Dumb Component : MAX 80 lignes (présentation)
  • Atomic Component : MAX 50 lignes (UI basique)

Si dépassé → Décomposer immédiatement

2. Single Responsibility Principle

Un composant = UNE responsabilité

// ❌ MAUVAIS - Responsabilités multiples
export function UserProfile() {
  // 1. Fetch data
  // 2. Display profile
  // 3. Edit form
  // 4. Settings panel
  // ... 200 lignes
}

// ✅ BON - Responsabilités séparées
export function UserProfilePage() {
  return (
    <>
      <UserProfileHeader />
      <UserProfileDetails />
      <UserProfileSettings />
    </>
  );
}

export function UserProfileHeader() { /* ... */ }
export function UserProfileDetails() { /* ... */ }
export function UserProfileSettings() { /* ... */ }

3. Extraction de la Logique

Règle : Si logique > 10 lignes → Extraire dans un custom hook ou util

// ❌ MAUVAIS - Logique dans le composant
export function ClubCreationForm() {
  const [step, setStep] = useState(1);
  const [clubData, setClubData] = useState({});

  const validateStep1 = () => {
    if (!clubData.name || clubData.name.length < 3) return false;
    if (!clubData.description) return false;
    return true;
  };

  const validateStep2 = () => {
    // 20 lignes de validation
  };

  // ... logique complexe
}

// ✅ BON - Logique extraite
export function ClubCreationForm() {
  const {
    step,
    clubData,
    goToNextStep,
    goToPreviousStep,
    updateClubData,
    validateCurrentStep,
  } = useClubCreationFlow();

  return (
    <FormWizard currentStep={step}>
      {/* Présentation pure */}
    </FormWizard>
  );
}

// Custom hook (logique extraite)
function useClubCreationFlow() {
  const [step, setStep] = useState(1);
  const [clubData, setClubData] = useState({});

  const validateCurrentStep = () => {
    return clubCreationValidators[step](clubData);
  };

  // ... logique

  return {
    step,
    clubData,
    goToNextStep,
    goToPreviousStep,
    updateClubData,
    validateCurrentStep,
  };
}

4. Composition > Monolithic

Privilégier la composition de petits composants

// ❌ MAUVAIS - Composant monolithique
export function MemberCard({ member }: Props) {
  return (
    <div className="card">
      <div className="avatar">
        <img src={member.avatar} />
      </div>
      <div className="info">
        <h3>{member.name}</h3>
        <p>{member.email}</p>
        <span className="role">{member.role}</span>
      </div>
      <div className="actions">
        <button onClick={onEdit}>Edit</button>
        <button onClick={onDelete}>Delete</button>
      </div>
    </div>
  );
}

// ✅ BON - Composé de petits composants
export function MemberCard({ member, onEdit, onDelete }: Props) {
  return (
    <Card>
      <MemberAvatar src={member.avatar} alt={member.name} />
      <MemberInfo name={member.name} email={member.email} role={member.role} />
      <MemberActions onEdit={onEdit} onDelete={onDelete} />
    </Card>
  );
}

// Composants réutilisables
function MemberAvatar({ src, alt }: AvatarProps) { /* ... */ }
function MemberInfo({ name, email, role }: InfoProps) { /* ... */ }
function MemberActions({ onEdit, onDelete }: ActionsProps) { /* ... */ }

🏗️ Structure des Composants

Organisation des Dossiers

features/
└── club-management/
    └── components/
        ├── ClubCreationForm.tsx          # Smart (orchestration)
        ├── ClubInfoStep.tsx              # Dumb (présentation step 1)
        ├── PlanSelectionStep.tsx         # Dumb (présentation step 2)
        ├── MembersList.tsx               # Smart (fetch + state)
        ├── MemberCard.tsx                # Dumb (présentation membre)
        ├── MemberAvatar.tsx              # Dumb (avatar)
        ├── MemberActions.tsx             # Dumb (boutons actions)
        └── shared/
            ├── FormWizard.tsx            # Réutilisable (wizard)
            └── StepIndicator.tsx         # Réutilisable (steps)

components/
└── ui/                                   # Atomic components (shadcn/ui)
    ├── button.tsx
    ├── input.tsx
    ├── card.tsx
    └── ...

Template Server Component (preferred)

// features/club-management/components/MembersList.tsx
// Server Component (pas de "use client")

import { getMembers } from '../api/members.server';
import { MemberCard } from './MemberCard';
import { MembersListSkeleton } from './MembersListSkeleton';

interface MembersListProps {
  clubId: string;
}

export async function MembersList({ clubId }: MembersListProps) {
  // ✅ Fetch server-side
  const members = await getMembers(clubId);

  // Empty state
  if (members.length === 0) {
    return <EmptyMembersList />;
  }

  // Render (délègue aux Client Components si besoin d'interactivité)
  return (
    <div className="space-y-4">
      <MembersListHeader count={members.length} />
      <div className="grid gap-4">
        {members.map(member => (
          <MemberCard
            key={member.id}
            member={member}
            clubId={clubId}
          />
        ))}
      </div>
    </div>
  );
}

Template Client Component (si interactivité nécessaire)

// features/club-management/components/MemberCard.tsx
"use client";

import { useTransition } from 'react';
import { removeMemberAction } from '../actions/remove-member.action';

interface MemberCardProps {
  member: Member;
  clubId: string;
}

export function MemberCard({ member, clubId }: MemberCardProps) {
  const [isPending, startTransition] = useTransition();

  // Event handler (mutation)
  const handleRemove = () => {
    startTransition(async () => {
      await removeMemberAction(clubId, member.id);
    });
  };

  return (
    <Card>
      <MemberInfo member={member} />
      <Button onClick={handleRemove} disabled={isPending}>
        {isPending ? "Suppression..." : "Retirer"}
      </Button>
    </Card>
  );
}

Template Dumb Component

// features/club-management/components/MemberCard.tsx

import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { MemberAvatar } from './MemberAvatar';

interface MemberCardProps {
  member: {
    id: string;
    name: string;
    email: string;
    role: string;
    avatar?: string;
  };
  onRemove: () => void;
}

export function MemberCard({ member, onRemove }: MemberCardProps) {
  return (
    <Card className="p-4 flex items-center gap-4">
      <MemberAvatar src={member.avatar} name={member.name} />

      <div className="flex-1">
        <h3 className="font-semibold">{member.name}</h3>
        <p className="text-sm text-muted-foreground">{member.email}</p>
        <span className="text-xs text-primary">{member.role}</span>
      </div>

      <Button
        variant="destructive"
        size="sm"
        onClick={onRemove}
      >
        Retirer
      </Button>
    </Card>
  );
}

// Sous-composant (si réutilisable ailleurs)
interface MemberAvatarProps {
  src?: string;
  name: string;
}

function MemberAvatar({ src, name }: MemberAvatarProps) {
  const initials = name
    .split(' ')
    .map(n => n[0])
    .join('')
    .toUpperCase();

  if (src) {
    return (
      <img
        src={src}
        alt={name}
        className="w-12 h-12 rounded-full object-cover"
      />
    );
  }

  return (
    <div className="w-12 h-12 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-semibold">
      {initials}
    </div>
  );
}

Template Page (Ultra-thin)

// app/(dashboard)/coach/clubs/[id]/members/page.tsx

import { Suspense } from 'react';
import { MembersList } from '@/features/club-management/components/MembersList';
import { MembersListSkeleton } from '@/features/club-management/components/MembersListSkeleton';

interface PageProps {
  params: { id: string };
}

export default function ClubMembersPage({ params }: PageProps) {
  return (
    <div className="container py-8">
      <h1 className="text-3xl font-bold mb-6">Membres du club</h1>

      <Suspense fallback={<MembersListSkeleton />}>
        <MembersList clubId={params.id} />
      </Suspense>
    </div>
  );
}

✅ Checklist Atomic Design

Avant de Créer un Composant

  • Le composant a-t-il UNE seule responsabilité ?
  • Est-il Smart (logique) ou Dumb (présentation) ?
  • Peut-il être décomposé davantage ?
  • Est-il réutilisable ailleurs ?
  • Les props sont-elles strictement typées ?
  • Le nom est-il descriptif ?

Pendant la Création

  • Logique extraite dans hooks/utils si > 10 lignes
  • JSX imbrication < 5 niveaux
  • Composant < 150 lignes
  • Aucun state dans Dumb Component (sauf UI: hover, focus)
  • Aucune logique métier dans Dumb Component
  • Composition privilégiée

Après la Création

  • Composant testé (si Smart)
  • Props documentées (JSDoc si nécessaire)
  • Accessible (ARIA, keyboard navigation)
  • Responsive (mobile-first)
  • Pas de warnings ESLint/TypeScript

🚨 Erreurs Courantes à Éviter

1. Composant Monolithique

// ❌ MAUVAIS - 300 lignes, fait tout
export function ClubDashboard() {
  // Fetch data
  // Display stats
  // Display members list
  // Display latest activities
  // Display settings panel
  // ... 300 lignes
}

// ✅ BON - Décomposé
export function ClubDashboard() {
  return (
    <>
      <ClubStats />
      <MembersList />
      <ActivitiesFeed />
      <SettingsPanel />
    </>
  );
}

2. Mélange Smart/Dumb

// ❌ MAUVAIS - Mélange logique + présentation
export function MemberCard({ member }: Props) {
  const [isPending, startTransition] = useTransition();

  const handleRemove = async () => {
    startTransition(async () => {
      await removeMemberAction(member.id);
    });
  };

  return <Card>...</Card>; // Présentation
}

// ✅ BON - Séparation claire
// Smart Component (logique)
export function MembersList() {
  const handleRemove = async (id: string) => {
    await removeMemberAction(id);
  };

  return members.map(m => (
    <MemberCard member={m} onRemove={() => handleRemove(m.id)} />
  ));
}

// Dumb Component (présentation)
export function MemberCard({ member, onRemove }: Props) {
  return <Card onClick={onRemove}>...</Card>;
}

3. Props Drilling Excessif

// ❌ MAUVAIS - Props drilling sur 5 niveaux
<GrandParent data={data}>
  <Parent data={data}>
    <Child data={data}>
      <GrandChild data={data} />
    </Child>
  </Parent>
</GrandParent>

// ✅ BON - Context ou Zustand store
const useDataStore = create((set) => ({
  data: null,
  setData: (data) => set({ data }),
}));

// Composants consomment directement
export function GrandChild() {
  const data = useDataStore(state => state.data);
  return <div>{data}</div>;
}

📚 Skills Complémentaires

Pour aller plus loin :

  • mobile-first : Design mobile-first principles
  • react-state-management : State management patterns
  • zero-warnings : Quality standards

Rappel : Décomposition maximale = Code maintenable, réutilisable et testable. Un composant = Une responsabilité.