Skills/component-skeleton-contract/SKILL.md

6.7 KiB

name description
component-skeleton-contract Enforce a strict synchronization contract between React components and their skeleton loading states. Requires co-located compound <Component.Skeleton /> exports, shared structural layouts, zero Cumulative Layout Shift (CLS = 0), OKLCH directional shimmer, and React 19 Suspense / loading.tsx composition.

Component Skeleton Synchronization Contract (Zero-CLS Architecture)

Ce skill impose un contrat architectural strict : chaque composant asynchrone ou dynamique doit posséder et exporter son propre Skeleton co-localisé (<Component.Skeleton />).

Il garantit que le squelette de chargement partage la même structure dimensionnelle que le composant réel, éliminant à 100% le décalage de mise en page (Cumulative Layout Shift - CLS), même lors des futures mises à jour du composant.


🎯 Déclencheurs d'Activation

Activez ce skill dès que :

  1. Vous concevez un composant qui charge des données distantes (cartes de profil, widgets de métriques, listes d'articles, tableaux).
  2. Vous créez un fichier de route de chargement (loading.tsx dans Next.js / Remix).
  3. Vous définissez une frontière asynchrone avec <Suspense fallback={...}>.
  4. Vous mettez à jour la géométrie ou les espacements d'un composant existant (le Skeleton doit être synchronisé immédiatement).
  5. Vous auditez le critère Core Web Vitals CLS ≤ 0.1.

📐 1. Le Contrat Architectural Co-localisé (Compound Pattern)

Les 4 Règles d'Or du Contrat :

  1. Même fichier source : Le Skeleton vit obligatoirement dans le même fichier que le composant réel.
  2. Export Compound attaché : Le composant principal expose son skeleton via la propriété statique <Component.Skeleton /> ET un export nommé.
  3. Conteneur Structurel Identique : Le conteneur racine du Skeleton doit avoir exactement les mêmes classes CSS de disposition (display, padding, gap, grid-template, min-height, border-radius) que le composant réel.
  4. Composition dans loading.tsx & <Suspense> : Il est formellement interdit de réinventer un faux layout dans un loading.tsx. Une page en chargement doit être l'assemblage exact des .Skeleton de ses composants enfants.

💫 2. Texture Visuelle : Shimmer Directionnel en OKLCH

Les skeletons ne doivent pas utiliser une simple alternance d'opacité grossière (animate-pulse). Ils adoptent un balayage de lumière feutré en OKLCH qui respecte l'accessibilité cognitive (prefers-reduced-motion).

Définition CSS du Shimmer (Tailwind v4 / CSS standard) :

@keyframes shimmer {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

.skeleton-shimmer {
  position: relative;
  overflow: hidden;
  background-color: oklch(0.2 0.01 260); /* Fond neutre sombre teinté */
}

.skeleton-shimmer::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    oklch(1 0 0 / 0.07) 50%,
    transparent 100%
  );
  animation: shimmer 1.8s infinite ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  .skeleton-shimmer::after {
    animation: none;
    background-image: none;
  }
}

💻 3. Exemple d'Implémentation de Référence

Voici un composant métier parfait respectant le contrat d'invariance dimensionnelle :

// components/user-profile-card.tsx
import React from 'react';

// Classes de disposition partagées pour garantir CLS = 0
const CONTAINER_LAYOUT = `
  relative overflow-hidden rounded-2xl p-6
  bg-[oklch(0.16_0.008_255)] border border-[oklch(1_0_0/0.08)]
  flex flex-col gap-4 min-h-[200px]
`;

export interface UserProfileCardProps {
  name: string;
  role: string;
  avatarUrl: string;
  stats: { followers: number; projects: number };
}

// 1. Le Composant Réel
export const UserProfileCard: React.FC<UserProfileCardProps> & {
  Skeleton: React.FC;
} = ({ name, role, avatarUrl, stats }) => {
  return (
    <article className={CONTAINER_LAYOUT}>
      <div className="flex items-center gap-4">
        <img
          src={avatarUrl}
          alt={name}
          className="size-14 rounded-full object-cover border border-white/10"
        />
        <div className="flex flex-col gap-1">
          <h3 className="font-semibold text-lg text-white leading-none">{name}</h3>
          <p className="text-sm text-white/60">{role}</p>
        </div>
      </div>
      <div className="mt-auto pt-4 border-t border-white/5 flex justify-between text-xs text-white/70">
        <span>{stats.projects} projets</span>
        <span>{stats.followers} abonnés</span>
      </div>
    </article>
  );
};

// 2. Le Skeleton Co-localisé (Géométrie strictly identique)
const UserProfileCardSkeleton: React.FC = () => {
  return (
    <div aria-hidden="true" className={`${CONTAINER_LAYOUT} animate-pulse`}>
      <div className="flex items-center gap-4">
        {/* Avatar Placeholder */}
        <div className="size-14 rounded-full bg-[oklch(0.24_0.01_260)] shrink-0" />
        
        {/* Textes Placeholders */}
        <div className="flex flex-col gap-2 flex-1">
          <div className="h-5 w-32 rounded-md bg-[oklch(0.24_0.01_260)]" />
          <div className="h-4 w-20 rounded-md bg-[oklch(0.20_0.01_260)]" />
        </div>
      </div>

      {/* Footer Placeholder */}
      <div className="mt-auto pt-4 border-t border-white/5 flex justify-between">
        <div className="h-3 w-16 rounded bg-[oklch(0.20_0.01_260)]" />
        <div className="h-3 w-16 rounded bg-[oklch(0.20_0.01_260)]" />
      </div>
    </div>
  );
};

// Liaison compound
UserProfileCard.Skeleton = UserProfileCardSkeleton;

🚀 4. Composition dans loading.tsx & React 19 Suspense

Lors de la création d'une page avec streaming, assemblez les squelettes directement :

// app/dashboard/loading.tsx
import { UserProfileCard } from '@/components/user-profile-card';

export default function DashboardLoading() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-8">
      <UserProfileCard.Skeleton />
      <UserProfileCard.Skeleton />
      <UserProfileCard.Skeleton />
    </div>
  );
}

📋 Checklist de Conformité Skeleton (Zero-CLS Gate)

  • Tout composant asynchrone exporte son sous-composant .Skeleton.
  • Le conteneur du Skeleton et celui du composant réel partagent les mêmes classes de grille, flex, marges intérieures (padding) et hauteur minimale.
  • aria-hidden="true" est apposé sur le conteneur du squelette pour ne pas perturber les lecteurs d'écran.
  • Le shimmer visuel se désactive automatiquement sous prefers-reduced-motion.
  • Le fichier loading.tsx de la route réutilise exclusivement les composants .Skeleton des briques de la page.