Skills/component-skeleton-contract/SKILL.md

184 lines
6.7 KiB
Markdown

---
name: component-skeleton-contract
description: >-
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) :
```css
@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 :
```tsx
// 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 :
```tsx
// 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.