Aller au contenu
§9.8

Documentation

Comment reprendre cette base pour un projet suivant. Ce document décrit ce qui existe réellement dans le dépôt, y compris les pièges qui ont coûté un cycle de correction — ils sont marqués comme tels.

Installation

npm install
npm run dev      # développement
npm run build    # build de production
npm run start    # sert le build

Une seule variable d'environnement est lue : NEXT_PUBLIC_SITE_URL. Tant qu'elle est absente, le site refuse d'être indexé. Voir .env.example.

Les trois contrôles se lancent séparément :

npm run typecheck      # TypeScript strict
npm run lint           # ESLint, doit sortir sans erreur NI avertissement
npm run verify:tokens  # motion.css et motion.ts sont-ils identiques ?
npm run verify         # navigateur : serveur devant déjà tourner

Règle

npm run verify charge chaque page aux neuf largeurs du §11, dans les deux thèmes, et échoue sur un débordement, un texte tronqué, une requête en 4xx, une cible tactile sous 24 px, ou un texte laissé transparent alors qu'il est visible. Un build vert ne remplace pas ce contrôle.

Architecture

Le dépôt sert deux produits sur un seul déploiement. Les groupes de routes portent le chrome, la racine ne porte que les providers.

src/
├── app/
│   ├── layout.tsx          polices, thème, providers — rien d'autre
│   ├── page.tsx            / redirige vers /fr
│   ├── (sentis)/[locale]/  site de l'agence : /fr et /en
│   ├── (showcase)/nexus/   vitrine : /nexus et ses sous-pages
│   └── (app)/nexus/        enveloppe applicative : /nexus/dashboard
├── components/
│   ├── ui/                 primitives vendues (shadcn + Magic UI)
│   ├── layout/             Container, Section, en-têtes, pieds de page
│   ├── motion/             bibliothèque d'animations
│   ├── sections/           blocs composés de la vitrine
│   └── sentis/             chrome propre au site de l'agence
├── lib/                    nav, i18n, site, motion, stats, utils
└── styles/
    ├── tokens.css          couleur, espacement, rayon, ombre, grille
    ├── motion.css          durées, courbes, reduced-motion
    ├── scroll-driven.css   animations natives liées au scroll
    └── sentis.css          peau de la marque Studio Sentis

Règle

sections/ consomme ui/ et layout/, jamais l'inverse. Une primitive qui importe un bloc composé devient impossible à réutiliser ailleurs.

Piège

Les imports entre pages d'un même groupe de routes sont relatifs (./demo), pas aliasés. Un alias @/app/(showcase)/… casse dès qu'on renomme le groupe, ce qui est arrivé.

Design tokens

Toute valeur visuelle vit dans src/styles/tokens.css. Un composant ne contient jamais de couleur, de rayon ni d'ombre en dur.

Règle

Aucune couleur n'entre dans les tokens sans avoir été mesurée. Chaque paire texte/surface passe au détecteur : .claude/skills/impeccable/scripts/impeccable detect <fichier>. Trois couleurs candidates ont été rejetées à ce stade.

Règle

La palette de graphiques suit une règle différente et plus stricte : elle se valide avec le script du skill dataviz, qui vérifie la bande de clarté, le plancher de chroma, la séparation daltonienne et le contraste. La palette d'origine du projet échouait trois de ces cinq contrôles.

Piège

--content-max est en ch : l'unité se résout sur la police de l'élément qui porte la classe. À poser sur le texte lui-même, jamais sur un conteneur dont la taille de police diffère.

Ajouter une marque

Le site de l'agence est la démonstration que la base sert à autre chose qu'elle-même : même architecture de tokens, mêmes composants, autre identité. Trois étapes.

/* 1. src/styles/<marque>.css — redéfinir les MÊMES noms de tokens */
[data-brand="sentis"] {
  --paper: #fcfaf6;
  --ink: #1a1714;
  --radius: 0.625rem;
  --content-max: 58ch;
}
.dark [data-brand="sentis"] { /* … */ }

/* 2. l'importer dans src/app/globals.css */
@import "../styles/sentis.css";
// 3. poser l'attribut sur le layout de la marque
<div data-brand="sentis" className={`${police.variable} bg-paper text-ink`}>
  {children}
</div>

Règle

Redéfinir les tokens existants, jamais en inventer de nouveaux pour une marque. Un token propre à une marque ne serait pas lu par les composants partagés, et la peau cesserait d'être interchangeable.

Composants

src/components/ui/ n'est pas écrit à la main : il est peuplé par npx shadcn@latest add. Le modifier signifie qu'une mise à jour du registre écrasera le travail.

Règle

Une correction dans un composant vendu est permise, mais son commentaire doit dire pourquoi. Trois cas existent dans le dépôt : un dégradé violet codé en dur, un setState dans un effet, et un compteur rendu à zéro côté serveur.

Règle

Un composant qui n'est utilisé nulle part est retiré, pas gardé au cas où. Quatre l'ont été : deux pour des défauts réels (Math.random() pendant le rendu, setState dans un effet), deux parce que rien ne s'en servait.

Animations

Durées, courbes, décalages et distances viennent de motion.css, dont src/lib/motion.ts est le miroir pour Motion, qui a besoin de nombres. npm run verify:tokens échoue si les deux divergent.

Règle

prefers-reduced-motion est traité une fois, globalement. Un composant ne rend jamais une version raccourcie de son animation : il rend l'état final.

Piège

Une plage useTransform liée au scroll doit tenir dans [0, 1]. Motion confie ces valeurs à l'API d'animation du navigateur, qui refuse un offset négatif ou supérieur à 1 et lève une erreur à chaque chargement.

Piège

L'opacité n'est pas un signal d'état pour du texte. Atténuer un bloc à opacity: 0.35 fait tomber son texte à 1,8:1. Le signal passe par un filet, une couleur d'accent ou une bordure.

Responsive

Neuf largeurs sont vérifiées : 320, 375, 390, 430, 768, 834, 1024, 1280 et 1440 px, dans les deux thèmes. Container porte la gouttière unique du projet ; c'est lui qui garantit que rien ne touche le bord de l'écran.

Piège

Un élément de grille hérite de min-width: auto : un tableau imposait sa largeur minimale à toute la piste, donc à toutes les cartes de la même rangée. min-w-0 sur l'élément de grille est le correctif.

Piège

Un point de rupture est une largeur réelle à tester. À exactement 768 px, l'en-tête affichait la navigation desktop complète pour 822 px de contenu.

Accessibilité

Ce qui est vérifié automatiquement : contraste des tokens, cibles tactiles d'au moins 24 px, absence de texte transparent dans le viewport, comportement sous mouvement réduit, absence d'erreur de console.

Règle

Les primitives viennent de Radix : comportement clavier, gestion du focus et rôles ARIA sont fournis, pas réimplémentés. Un composant maison qui remplace une primitive doit reprendre le motif ARIA correspondant.

Règle

Une seule exemption est assumée : le contraste d'un contrôle désactivé. WCAG 1.4.3 exclut explicitement les composants d'interface inactifs, et le contrôle automatique les ignore pour cette raison.

Performance

Toutes les pages sont statiques. GSAP, Lenis et Three.js ne sont pas installés : les sections épinglées reposent sur position: sticky et le scroll horizontal sur scroll-snap natif.

Piège

Un accès disque dans un composant serveur doit utiliser des chemins littéraux. Un chemin construit dynamiquement empêche l'analyse statique et fait tracer tout le projet, dossier public/ compris, dans le bundle serveur.

Conventions de code

TypeScript strict, aucun any. Composants serveur par défaut ; "use client" seulement pour un état, un écouteur ou une API navigateur.

Règle

npm run lint doit sortir sans erreur ni avertissement. La règle react-hooks interdit setState dans un effet : le remplaçant est useSyncExternalStore, utilisé à deux endroits du dépôt.

Règle

Les commentaires expliquent pourquoi, pas quoi. Un commentaire qui paraphrase la ligne suivante est du bruit ; un commentaire qui explique une contrainte évite qu'on la casse.

Créer une page

1. src/app/(showcase)/nexus/<nom>/page.tsx
   export const metadata = { title, description }

2. Structure : <Section index title lead> pour chaque bloc.
   La gouttière vient de Container, jamais d'un padding local.

3. Partie interactive dans un fichier voisin en "use client",
   importé relativement (./panels), pas via un alias.

4. src/lib/nav.ts : ajouter l'entrée, status "planned" puis "live".
   Le site affiche l'état réel — une page annoncée finie ne l'est pas
   tant que l'entrée n'est pas passée à "live".

5. tests/responsive-check.mjs : ajouter la route à PAGES.

6. npm run lint && npm run typecheck && npm run build
   npm run start &  puis  npm run verify

Règle

Étape 5 non facultative. Une page absente du contrôle est une page dont personne ne saura qu'elle déborde à 320 px.

Contribution

L'état réel du projet, ce qui reste ouvert et ce qui a été assumé sont dans la vitrine et dans les documents du dépôt : docs/audit.md, docs/architecture.md et docs/quality-checklist.md.

Règle

Un travail n'est pas terminé parce que le code compile. Le cahier des charges le dit, et trois défauts de ce projet l'ont confirmé : un titre invisible sans JavaScript, des compteurs rendus à zéro, et un dégradé violet en haut de chaque page. Aucun n'a été signalé par le build.