# 21 — Tests et Storybook

**Statut : fait · Phase : transverse · Mis à jour : 30/10/2026.**

Décidé le 29/10/2026. Chaque composant et chaque runtime de Récit reçoit une **story** qui le
spécifie état par état, une **fonction `play`** qui prouve son comportement, un contrôle
d'**accessibilité** et une **capture** en clair et en sombre. Storybook devient la spécification
exécutable de l'interface : ce que la migration de style (`19-style.md`) transforme, ces tests le
vérifient ; ce qui vient après s'écrit d'abord dans une story. Le plan inventorie l'existant (§1),
fixe la pyramide et l'outil (§2), structure Storybook (§3), dresse le catalogue des stories (§4),
les conventions (§5), la mesure (§6), le câblage CI (§7), puis les lots et leur imbrication dans les
vagues du plan 19 telles que `20-multi-agents.md` §4 les redécoupe (§8). Ce plan **ne s'implémente
pas** avant la demande de l'utilisateur.

## 0. Décision, portée et lecture de la demande

### 0.1 La demande, telle qu'elle est lue

- « Élaborer des tests » : des tests **fonctionnels**, qui prouvent ce que fait un composant (un
  clic ouvre, une touche navigue, un état s'affiche), et pas seulement qu'il se rend sans exception.
- « Structurer Storybook » : une arborescence par surface, des décorateurs communs, une story par
  composant avec ses états, et Storybook comme point d'entrée des tests de composants, pas comme une
  vitrine à part.
- « Le plus de tests fonctionnels sur le sujet » : le sujet est l'interface de Récit telle que le
  plan 19 la réécrit — les primitives `ui/` du framework, ses composants et sa navigation, les
  composants de diapositive, le runtime du deck, les blocs motion, `site-kit`, les composants des
  sites et des exemples. Le code non visuel (compilation, config, CLI) garde ses tests Vitest, ce
  plan ne les refait pas.

### 0.2 Objectifs

- **Une story par composant exporté**, avec un export par état (défaut, variantes, vide, erreur,
  chargement, long contenu), les `args` typés, l'autodoc.
- **Une fonction `play` par comportement** : ouverture et fermeture, clavier, focus, sélection,
  copie, navigation ; écrite contre les rôles et les noms accessibles, jamais contre une classe.
- **L'accessibilité par story** (`addon-a11y` en mode test, déjà `a11y.test: "error"`), et le test
  axe des pages e2e conservé.
- **Le visuel par story** : une capture en clair et en sombre de chaque story, dans le conteneur
  Linux, par la spec `e2e/storybook.spec.ts` que STY1 a posée ; les stories sont la galerie
  avant/après du plan 19 au grain du composant.
- **Un seul outil de plus** : l'addon Vitest de Storybook, qui exécute les stories dans Chromium par
  Playwright, déjà présent. Pas de Jest, pas de second runner.
- **Une mesure** : un compteur des composants sans story et sans `play`, en avertissement puis en
  erreur, comme le compteur de style du plan 19 ; un cliquet de couverture Vitest par paquet.

### 0.3 Non-objectifs

- Pas de refonte des tests unitaires existants (1 591 tests, 154 fichiers) : ils restent, on
  n'ajoute que ce qui manque.
- Pas de tests de rendu Remotion image par image hors des goldens existants (16 clichés).
- Pas de tests de charge, pas de tests de contrat réseau : Récit n'a pas de serveur.
- Pas de couverture à 100 % comme objectif chiffré : la cible est « chaque composant a sa story,
  chaque comportement son `play` », mesurée par le compteur de §6, pas par un pourcentage de lignes.

### 0.4 La pyramide cible

| Niveau        | Quoi                                                                                                   | Outil                                                   | Où ça tourne                     |
| ------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- | -------------------------------- |
| Unitaire      | logique pure : parseurs, config, navigation du deck, horloge motion, scripts                           | Vitest (node, `happy-dom` au besoin)                    | `pnpm test`                      |
| Composant     | chaque story se rend, ses `args` tiennent, aucune erreur console                                       | Storybook + addon Vitest, navigateur Chromium           | `pnpm test` (projet `storybook`) |
| Interaction   | fonctions `play` : clavier, souris, focus, états                                                       | `storybook/test` (`userEvent`, `within`, `expect`)      | idem                             |
| Accessibilité | axe sur chaque story ; axe sur trois pages e2e                                                         | `addon-a11y` en mode test ; `@axe-core/playwright`      | idem ; conteneur e2e             |
| Visuel        | une capture par story, clair et sombre ; les pages de site ; les diapositives                          | Playwright sur `storybook-static` et sur les sites      | conteneur e2e (`-linux.png`)     |
| Flux          | ce qu'une story ne peut pas prouver : recherche puis navigation, thème persistant, tiroir mobile, deck | Playwright, specs `e2e/*.spec.ts` sans capture          | conteneur e2e                    |
| Films         | un cliché par bloc, deux films, deux thèmes                                                            | goldens de `@recit/motion-render` (`pixelmatch`, 0,2 %) | `build.yaml` « Motion goldens »  |
| Export        | PDF d'un deck : nombre de pages, dimensions                                                            | `deck-features.spec.ts` (existant)                      | conteneur e2e                    |

## 1. Inventaire avant

### 1.1 L'outillage Vitest

- **Racine** `vitest.config.ts` : trois globs de projets (`packages/!(formats|integrations|tools)`,
  `packages/{formats,integrations,tools}/*`, `scripts`), couverture `v8` avec quatre rapporteurs
  dans `./coverage`, **sans seuil**. Scripts : `pnpm test` = `vitest run --typecheck`,
  `test:coverage` ; `e2e` est un projet nx à part (`recit-e2e`, `scope:test`).
- **Configs de projet** : `packages/core` et `site-stories` (`include` des `*.test-d.ts` pour le
  typecheck), `packages/recit`, `packages/tools/site-kit`, `packages/site` (`globals`,
  `setupFiles: vitest.setup.ts` qui étend `expect` avec `@testing-library/jest-dom` et appelle
  `cleanup()` après chaque test, `typecheck` par `tsgo` sur `tsconfig.app.json`).
- **Environnements** : node par défaut ; `happy-dom` 21.0.4 déclaré par fichier
  (`@vitest-environment happy-dom`) dans 21 fichiers de `packages/site` ; aucun `jsdom`.
- **Dépendances de test** (catalogue) : `@testing-library/dom` 10.4.2, `jest-dom` 6.10.0, `react`
  16.3.3, `user-event` 14.6.7, utilisées par `packages/site` seulement ; `@vitest/coverage-v8`
  4.1.11 ; Vitest 4.1.11 ; `@axe-core/playwright` à la racine ; Playwright 1.64.1.
- **Volume** (29/10) : 154 fichiers, 1 591 tests, en 2 min 30 environ dans `pnpm verify`.

| Projet           | Fichiers de spec | Ce qu'ils couvrent                                                                                                              |
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `core`           | 23               | parseur, schémas, config (`app:` inclus), conversion source, JSON Schema                                                        |
| `site`           | 56               | 24 `.test.tsx` de composants (ci-dessous), config, routes, SSR, plugins, hooks                                                  |
| `motion`         | 16               | schéma, timeline, adaptateurs                                                                                                   |
| `site-stories`   | 11               | plugin, config de site, i18n, composants de récit, motion                                                                       |
| `deck`           | 10               | navigation, `first-render` (chaîne SSR), `presentation-a11y` (`role="application"`), `mdx-slide`, transitions, `peer-messenger` |
| `app`            | 8                | config Tauri, coquille, serveur, Android, iOS, deck                                                                             |
| `motion-blocks`  | 6                | `player`, `render`, horloge, thèmes                                                                                             |
| `cli`, `mcp`     | 5 + 5            | commandes, bundling, serveur MCP                                                                                                |
| `components`     | 4                | `code-block`, `mermaid-render`, `presets`, `slides` (rendu HTML des composants de diapositive)                                  |
| `site-kit`       | 4                | config, énumérations, sites, workspace ; **aucun composant**                                                                    |
| `scripts`        | 6                | frontières, version plans, e2e-linux, paquets publiés, workflows, galerie                                                       |
| `theme`, `recit` | 1 + 1            | thèmes, point d'entrée                                                                                                          |
| `motion-render`  | 0                | les goldens (§1.3) tiennent lieu de test                                                                                        |

### 1.2 Les tests de composants existants

Vingt-quatre `.test.tsx` dans `packages/site/src` : `app/entry.server`, `processRoutes`,
`createRedirectRoutes`, `lib/components/BrowserWindow`, `Footer`, `Header`, `LandingPage`, `Main`,
`Markdown`, `MobileTopNavigation`, `NotFoundPage`, `PluginHeads`, `ScrollableTable`, `Search`,
`Slot`, `TopNavigation`, `navigation/Navigation`, `core/transform-config`, `hooks/useEvent`,
`plugins/markdown/MdxPage`, `search-inkeep`, `search-pagefind`, `ui/CodeBlock`, `ui/Stepper`. Chacun
monte son harnais à la main : `MemoryRouter` ou `createMemoryRouter` + `RouterProvider`,
`QueryClientProvider` avec un `QueryClient` neuf, le contexte de config moqué
(`Header.test.tsx:30-45`, `Footer.test.tsx:15-42`). **Aucun helper de rendu partagé**, aucune story
réutilisée. Dans `deck`, `first-render.spec.tsx` et `presentation-a11y.spec.tsx` rendent par
`renderToString` et lisent le HTML ; `components/src/__tests__/slides.spec.ts` vérifie le HTML des
composants de diapositive. Rien ne teste un comportement au clavier ou à la souris hors e2e.

### 1.3 Les goldens motion

`packages/formats/motion-render/scripts/goldens.mjs` : un cliché par bloc au milieu de chaque scène,
pour deux films de démonstration (`warm-editorial` 16:9, `saas-product` 9:16), rendus depuis le
`film.json` **et** depuis le jumeau Markdown ; 16 PNG dans `fixtures/goldens`, diff `pixelmatch` à
0,2 % de pixels, les diffs écrits dans `fixtures/goldens-diff` ; étape « Motion goldens » de
`build.yaml`, diffs envoyés en artefact. C'est le seul test visuel hors Playwright.

### 1.4 La suite e2e après STY1

- 15 specs dans `e2e/`, **443** références `-linux.png` (213 avant STY1), trois projets Playwright :
  `chromium` (1280×720), `chromium-dark` (tous les `site-*.spec.ts`, `colorScheme: "dark"` et le
  choix mémorisé `theme`/`recit-theme`, suffixe `-dark`), `mobile` (390×844, tests `@mobile` de
  `site-docs`, suffixe `-mobile`) ; `maxDiffPixelRatio` 0,02, animations désactivées ; rapporteur
  `json` dans `test-results/linux/report.json`.
- `storybook.spec.ts` : les **16 stories** de `index.json`, en clair et en sombre par
  `iframe.html?id=…&globals=theme:…`, servies depuis `sites/storybook/storybook-static` sur le port
  4380 ; **32** références ; la liste des ids est **codée en dur** dans la spec (`STORIES`), à
  maintenir à chaque story.
- `accessibility.spec.ts` : axe WCAG 2 A/AA et 2.1 A/AA sur trois URL.
- Un passage complet en conteneur : **7 à 8 min** (copie 1 s, installation 25 à 45 s, construction 2
  à 2 min 30, tests 4 min 20 à 5 min 20 pour 444 tests) ; galerie
  `node scripts/screenshot-gallery.js --serve <port>` ; règle du skill `e2e-baselines` : deux
  passages, régénération par `test:e2e:update:linux <spec>` seulement.

### 1.5 Storybook

- `sites/storybook` (`scope:site`, privé) : Storybook **10.6.3** (10.6.5 publié), `react-vite`,
  addons `docs`, `a11y` (`a11y.test: "error"`), `themes` (`withThemeByClassName`, `light: ""`,
  `dark: "dark"`), `typescript.reactDocgen: "react-docgen"`, `tags: ["autodocs"]`,
  `controls.expanded`. Scripts `dev` (port 6006, `--no-open`), `build` (`storybook-static`),
  `typecheck`. Construit par `pnpm build` (nx) : la spec e2e le sert.
- Stories : **9 fichiers, 16 stories**, toutes dans `packages/components/src/slides/` (`Callout`,
  `CodeBlock`, `CodeGroup`, `Cover`, `Grid`, `Media`, `Mermaid`, `Points`, `Quote`), titres
  `Slides/<Composant>`, `satisfies Meta<typeof X>`, `args` et `argTypes` renseignés
  (`Callout.stories.tsx:5-16`). Le glob de `main.ts` ne lit que `packages/components`.
- Décorateurs : `withSlide` (cadre 16:9, `SlideMotion`, `parameters.slide.fit`) avec **deux `style`
  inline** et deux classes (`preview.tsx:14-32`, le plan 19 STY6.1 le réécrit en module) ;
  `withThemeByClassName`. `preview.css` importe Tailwind, les polices, les tokens, le pont Tailwind,
  `page.css`, `deck.css`, et déclare `@source` sur `packages/components/src`.
- **Absents** : addon Vitest, test-runner, fonctions `play`, `storybook/test`, `composeStories`,
  presets de viewport, décorateur de fournisseurs de site (routeur, query, config), stories hors
  `packages/components`.

### 1.6 Sans test et sans story

| Surface                                             | Composants                                                                                                                                                                      | Test aujourd'hui                               |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `packages/site/src/lib/ui`                          | 54 composants (57 fichiers), dont 27 sans importeur que STY3 supprime ; **27 restent** (§4.1)                                                                                   | `CodeBlock.test.tsx`, `Stepper.test.tsx`       |
| `packages/site/src/lib/components` (+ `navigation`) | 50 fichiers : `Header`, `Footer`, `Banner`, `Search`, `Pagination`, `ThemeSwitch`, `Mermaid`, `Markdown`, `Typography`, `Heading`, 12 de navigation…                            | 14 `.test.tsx` de rendu, aucun comportement    |
| `packages/site/src/lib/plugins`                     | `MdxPage`, `PagefindSearch`, `ResultList`, `IndexingDialog`, `search-inkeep`                                                                                                    | 3 `.test.tsx`                                  |
| `packages/formats/deck`                             | 10 composants : `Presentation`, `SlideStage`, `SlideFrame`, `PresenterView`, `MobileRemoteView`, `RemoteQrCodeModal`, `AnnotationCanvas`, `DevHud`, `HtmlSlide`, `SceneContent` | 10 specs sans interaction, e2e `deck-features` |
| `packages/formats/motion-blocks`                    | 8 blocs (`chapter`, `compareBars`, `list`, `stat`, `statement`, `title`, `uiMock`, `uiScene`), `player`                                                                         | `player.test.tsx`, `render.test.tsx`, goldens  |
| `packages/components`                               | 9 avec story, **6 sans** : `Card`, `Columns`/`Col`, `SlideImage`, `Badge`, `Kbd`, `Step` (`mdx/builtins.tsx`), `SlideMotion`                                                    | `slides.spec.ts` (HTML)                        |
| `packages/tools/site-kit`                           | `Cards`, `ExampleHeader`, `SourceFile`, `Tutorial`                                                                                                                              | aucun                                          |
| Sites et exemples                                   | `www` (`Hero`, `FormatGrid`, `OneSource`), `docs/StatusBadge`, `motion/VideoDownloads`, `components/FormatSupport`, `deck-mdx` (`Counter`, `Timeline`), `deck-showcase/Metric`  | captures de pages seulement                    |

### 1.7 Le budget CI

`build.yaml` tourne sur `ubuntu-latest` dans l'image `mcr.microsoft.com/playwright:v1.64.1-noble` :
`pnpm check`, `pnpm test`, publint et taille des bundles, goldens motion, `pnpm test:e2e`, rapport
Playwright en artefact ; un second job vérifie les liens. Pas de durée mesurée dans le dépôt ; sur
la machine de travail, `pnpm verify` prend 3 min 20 et un passage e2e 7 à 8 min : l'ordre de
grandeur du job est **12 à 15 min**. Chromium est dans l'image : les tests de stories en navigateur
n'y ajoutent aucune installation.

## 2. Cible : la pyramide et l'outil

### 2.1 Les trois façons de tester une story, évaluées

| Option                                                                                                                                                                  | Ce que ça donne                                                                                                                                                    | Dépendances                                                                                                   | Verdict                                                                                                                                                     |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`@storybook/addon-vitest`** : chaque story devient un test Vitest, exécuté dans Chromium (mode navigateur de Vitest, fournisseur Playwright) ; `play` et a11y compris | rendu réel (mise en page, portails, focus), un seul runner avec les tests unitaires, projet Vitest à part, rapport unifié, `storybook/test` fourni par `storybook` | `@storybook/addon-vitest` 10.6.x, `@vitest/browser-playwright` 4.1.x (Vitest 4.1 du dépôt) ; Chromium déjà là | **Retenu.** Deux dépendances de dev, aucun runner nouveau, le navigateur est celui des e2e                                                                  |
| `@storybook/test-runner` 0.26 : Jest + Playwright qui visite chaque story de `storybook-static`                                                                         | rendu réel aussi, mais un second runner (Jest) et un second rapport, Storybook construit avant                                                                     | `@storybook/test-runner`, `jest`, `jest-playwright`                                                           | **Écarté** : double la pile de test (règle « une seule stack »), plus lent (build + visite)                                                                 |
| Stories portables : `composeStories` de `@storybook/react-vite` dans un test Vitest `happy-dom`                                                                         | réutilise une story comme fixture d'un test unitaire, sans navigateur                                                                                              | aucune                                                                                                        | **Complément**, pas la base : `happy-dom` n'a ni mise en page ni focus fiable ; réservé à la logique qui a besoin d'une story comme entrée (deck, tableaux) |

Décision : **addon Vitest**, les stories portables en complément. Le mode navigateur tourne
`headless` ; en local il faut une fois `pnpm exec playwright install chromium` (déjà requis par
`pnpm test:e2e`, documenté dans CONTRIBUTING par TST5).

### 2.2 Le projet Vitest `storybook`

- `sites/storybook/vitest.config.ts` (nouveau) : `defineProject` avec le plugin
  `storybookTest({ configDir: ".storybook" })`,
  `test.browser = { enabled: true, headless: true, provider: playwright(), instances: [{ browser: "chromium" }] }`,
  `setupFiles` avec le `vitest.setup.ts` de Storybook (`setProjectAnnotations` des annotations de
  `preview.tsx`).
- `vitest.config.ts` racine : `sites/storybook` ajouté aux `projects` ; `pnpm test` l'exécute avec
  les autres, `pnpm test --project storybook` l'isole. Les stories n'entrent pas dans la couverture
  v8 des paquets (fichiers `*.stories.tsx` exclus), mais le code des composants qu'elles rendent, si
  : c'est ce qui fait monter la couverture de `packages/site` sans écrire un test par composant.
- Tags : `parameters.a11y.test = "error"` (déjà) fait échouer une story sur une violation axe ; une
  story qui n'a pas de sens en test porte `tags: ["!test"]` (jamais pour cacher un échec : la raison
  dans la story).
- `storybook/test` : `expect`, `within`, `userEvent`, `fn`, `waitFor` ; `fn()` pour chaque rappel
  (`onClick`, `onChange`) afin de l'asserter.

### 2.3 Le visuel

`e2e/storybook.spec.ts` passe de la liste codée en dur à la lecture de `storybook-static/index.json`
: **chaque** story reçoit ses deux captures sans toucher à la spec ; les stories qui se stabilisent
tard (Mermaid attend `mermaidReady`, le `Quote` sa transition) portent un **tag** (`wait-mermaid`,
`wait-motion`) que la spec lit dans `index.json`, qui n'expose pas les `parameters`. Les captures
suivent la règle du skill `e2e-baselines` (conteneur, deux passages, galerie, régénération par
spec). Une story qui change d'aspect pendant le plan 19 se régénère à la porte de la vague, après
revue de galerie : c'est le « comparatif avant/après » au grain du composant que l'utilisateur
demandait au plan 19.

### 2.4 Les flux

Les comportements qu'une story ne peut pas prouver seule (plusieurs pages, stockage, chargement
d'index) gardent une spec Playwright **sans capture**, dans `e2e/` : recherche ouverte au clavier,
résultat choisi, page atteinte ; thème changé puis conservé au rechargement ; tiroir mobile ouvert,
lien suivi, tiroir fermé ; sommaire qui suit le défilement ; deck : flèches, `?slide=N`, console
présentateur ouverte en seconde fenêtre, impression. Ce « filet comportemental » (TST-A) s'écrit
**avant** que le plan 19 ne touche ces composants (vague 2 de `20-multi-agents.md` §4), contre les
rôles et les noms : il survit à la migration et la prouve.

## 3. La structure de Storybook

### 3.1 Arborescence des titres

| Titre                         | Source                                                             | Décorateurs                                             |
| ----------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------- |
| `Site/UI/<Composant>`         | `packages/site/src/lib/ui/*.stories.tsx`                           | `withMantine`, `withTheme`                              |
| `Site/Components/<Composant>` | `packages/site/src/lib/components/*.stories.tsx`                   | `withMantine`, `withTheme`, `withSite`                  |
| `Site/Navigation/<Composant>` | `…/lib/components/navigation/*.stories.tsx`                        | idem                                                    |
| `Site/Plugins/<Plugin>`       | `…/lib/plugins/**/*.stories.tsx`                                   | idem                                                    |
| `Slides/<Composant>`          | `packages/components/src/**/*.stories.tsx` (existant)              | `withSlide`, `withTheme`                                |
| `Deck/<Composant>`            | `packages/formats/deck/src/**/*.stories.tsx`                       | `withDeck` (un `Story` compilé en fixture), `withTheme` |
| `Motion/Blocks/<Bloc>`        | `packages/formats/motion-blocks/src/blocks/*.stories.tsx`          | `withFilm` (`StaticAnim`, thème motion)                 |
| `Kit/<Composant>`             | `packages/tools/site-kit/src/components/*.stories.tsx`             | `withMantine`, `withTheme`, `withSite`                  |
| `Sites/<site>/<Composant>`    | `sites/*/src/*.stories.tsx`, `examples/*/components/*.stories.tsx` | selon la surface                                        |

Les globs correspondants entrent dans `main.ts` dès TST0, même vides : un fichier de story posé plus
tard apparaît sans configuration.

### 3.2 Décorateurs communs (`sites/storybook/.storybook/decorators/`)

- `withMantine` : `MantineProvider` avec `createRecitTheme` (plan 19 STY2) et la feuille Mantine en
  couche ; avant STY2, un `createTheme({})`.
- `withTheme` : le mode de couleur par `globals.theme`, `withThemeByClassName` jusqu'à STY9 puis
  `withThemeByDataAttribute` (`data-mantine-color-scheme`) ; la spec e2e lit le même `globals`.
- `withSite` : ce que `SiteRoot` fournit, en fixtures — `createMemoryRouter` sur une route
  paramétrable (`parameters.site.path`), `QueryClientProvider` neuf, le contexte de config avec
  `sites/storybook/src/harness/config.ts` (un `recit.config` de fixture, Lorem Ipsum), les slots,
  `MDXProvider` ; les **modules virtuels** du framework (`virtual:recit-config`,
  `virtual:recit-theme.css`, navigation, routes) sont fournis par un plugin Vite de `main.ts`
  (`viteFinal`) qui les résout vers `src/harness/virtual/*` — c'est le point dur de TST0, prouvé par
  la story pilote `Site/Components/Header`.
- `withSlide` : l'existant, réécrit en module par STY6.1 ; `parameters.slide.fit`.
- `withDeck` : un `Story` compilé depuis une fixture Markdown (`examples/deck-starter`) et le
  `DeckApp` ou la `Presentation` montée à la diapositive `parameters.deck.slide`.
- `withFilm` : `AnimContext` en `StaticAnim`, `ThemeVars` du thème motion choisi par
  `globals.motionTheme` (`warm-editorial`, `saas-product`), cadre 16:9 ou 9:16.
- `withReducedMotion` : `MotionConfig reducedMotion="always"` et `prefers-reduced-motion` émulé pour
  les captures et les `play` (les transitions de `motion/react` ne sont pas ce qu'on teste).
- Viewports : presets `mobile` 390×844, `tablet` 768×1024, `desktop` 1280×720, déclarés une fois ;
  une story mobile porte `parameters.viewport.defaultViewport`.

### 3.3 `args`, `argTypes`, autodoc

`satisfies Meta<typeof X>` et `StoryObj<typeof meta>` partout (le modèle est `Callout.stories.tsx`)
; `args` complets sur `meta`, chaque export ne pose que ce qui change ; `argTypes` avec `control`
pour les énumérations et `action` pour les rappels (`fn()`), `options` depuis les constantes du code
(`CALLOUT_TYPES` de `@recit/core/browser`, jamais recopiées) ; `tags: ["autodocs"]` global ;
`parameters.docs.description` en une phrase quand le nom ne suffit pas. Contenu Lorem Ipsum, jamais
de nom réel (règle des exemples).

### 3.4 Storybook et le paquet privé `@recit/site`

`sites/storybook` porte `scope:site` et peut dépendre de tout
(`scripts/check-package-boundaries.js`, `ALLOWED.site`) ; l'interdiction d'importer `@recit/site` ne
vaut que sous `packages/**` (`biome.jsonc`, `noRestrictedImports`). Les stories du framework vivent
**à côté des composants** dans `packages/site/src` avec des imports relatifs, et `sites/storybook`
ajoute `@recit/site` en `devDependencies` pour que le typecheck et le glob les voient ; nx reçoit
l'arête (règle ST-20). De même pour chaque site et chaque exemple dont Storybook lit une story
(`sites/www`, `examples/deck-mdx`…) : une `devDependency` par source de stories, sinon un build
affecté peut sauter Storybook.

## 4. Le catalogue des stories

Chaque ligne : les états (un export par état), les interactions (`play`), l'accessibilité (ce qu'axe
doit voir), le visuel (ce qui mérite une capture au-delà du défaut). Les composants que STY3
supprime (27 `ui/` sans importeur) n'y sont pas.

### 4.1 `Site/UI` — les 27 primitives conservées (TST2)

| Composant                                  | États                                                            | Interactions (`play`)                                                            | A11y                                    | Visuel                   |
| ------------------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------- | ------------------------ |
| `Button`, `ButtonGroup`                    | variantes, tailles, icône, désactivé, chargement, `asChild`/lien | clic (`fn`), Entrée et Espace, désactivé ne déclenche pas, focus visible         | nom accessible, `aria-disabled`         | focus ring, groupe       |
| `Badge`, `Kbd`                             | variantes                                                        | —                                                                                | contraste                               | défaut                   |
| `Toggle`                                   | pressé / non, désactivé                                          | clic bascule `aria-pressed`, Espace                                              | `aria-pressed`                          | les deux états           |
| `Input`, `InputGroup`, `Textarea`, `Label` | vide, valeur, invalide, désactivé, avec addon                    | saisie (`userEvent.type`), `onChange` appelé, label clique le champ              | association label/champ, `aria-invalid` | invalide                 |
| `Separator`                                | horizontal, vertical, décoratif                                  | —                                                                                | `role="separator"` ou `none`            | —                        |
| `Card`                                     | en-tête, contenu, pied, cliquable                                | —                                                                                | titre                                   | défaut                   |
| `Alert`, `Callout`                         | chaque type/couleur, avec et sans titre, long contenu            | —                                                                                | `role="alert"` ou `note`                | tous les types           |
| `Dialog`, `Drawer`                         | fermé, ouvert, avec formulaire, non fermable par clic extérieur  | ouvrir, Échap ferme, clic extérieur, focus piégé puis rendu au déclencheur       | `aria-modal`, titre, focus              | ouvert (clair et sombre) |
| `Popover`, `Tooltip`                       | fermé, ouvert, positions                                         | survol et focus ouvrent le tooltip, clic ouvre le popover, Échap ferme           | `aria-describedby`, `role="tooltip"`    | ouvert                   |
| `DropdownMenu`                             | items, séparateurs, raccourcis, sous-menu, désactivé             | ouvrir au clic, flèches, Entrée sélectionne (`fn`), Échap ferme                  | `role="menu"`, `menuitem`               | ouvert                   |
| `Command` (→ Spotlight)                    | vide, résultats, aucun résultat, groupes                         | saisie filtre, flèches, Entrée choisit, Échap ferme, `Ctrl+K`                    | `role="dialog"`, `listbox`/`option`     | ouvert avec résultats    |
| `Collapsible`                              | fermé, ouvert                                                    | clic bascule, `aria-expanded`                                                    | `aria-expanded`, `aria-controls`        | les deux                 |
| `NavigationMenu`                           | items, actif, déroulant                                          | survol et clavier                                                                | `role="navigation"`                     | déroulé                  |
| `CodeBlock`, `EmbeddedCodeBlock`           | langage, titre, lignes surlignées, long, bouton copier           | copier écrit dans le presse-papiers (`navigator.clipboard` moqué), retour visuel | bouton nommé                            | surlignage, sombre       |
| `CodeTabs`, `CodeTabPanel`                 | 2 et 5 onglets, onglet mémorisé                                  | clic et flèches changent d'onglet, `aria-selected`                               | `role="tablist"`, `tab`, `tabpanel`     | onglet actif             |
| `SyntaxHighlight`                          | chaque langue configurée, thème clair et sombre                  | —                                                                                | contraste des tokens                    | les deux thèmes          |
| `Stepper`                                  | 1, 3 et 10 étapes, étape courante                                | —                                                                                | liste ordonnée                          | défaut                   |

### 4.2 `Site/Components`, `Navigation`, `Plugins` (TST3)

| Composant                                                              | États                                                                           | Interactions                                                                             | A11y                             |
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------- |
| `Header`, `TopNavigation`, `HeaderNavigation`                          | logo, liens, actif, avec et sans recherche, bannière, mode sombre               | lien actif marqué, `ThemeSwitch` bascule le mode (`globals` non requis), recherche ouvre | `banner`, `navigation` nommée    |
| `MobileTopNavigation`, `MobileTopNavigationDrawer`                     | fermé, ouvert, arborescence profonde                                            | bouton ouvre, lien ferme, Échap ferme, focus piégé                                       | `aria-expanded`, `dialog`        |
| `Footer`, `Banner`                                                     | 1 à 3 colonnes, logo, variables, bannière fermable                              | fermer la bannière, mémorisation                                                         | `contentinfo`                    |
| `Search`, `PagefindSearch`, `ResultList`, `IndexingDialog`             | fermé, ouvert, vide, résultats (index Pagefind de fixture), indexation en cours | `Ctrl+K`, saisie, flèches, Entrée navigue (`fn` sur le routeur), Échap                   | `combobox`, `listbox`            |
| `Sidebar`, `NavigationItem`, `SidebarToggle`, `Toc`                    | plié, déplié, item actif, catégories imbriquées, sommaire à 3 niveaux           | plier/déplier, clavier, le `Toc` suit `IntersectionObserver` (moqué)                     | `tree` ou `list`, `aria-current` |
| `Pagination`, `Anchor`, `CategoryHeading`                              | première, milieu, dernière page ; ancre survolée                                | lien d'ancre copie l'URL                                                                 | nav nommée                       |
| `PageProgress`, `Spinner`                                              | 0, 50, 100 %                                                                    | —                                                                                        | `progressbar`                    |
| `BrowserWindow`, `Framed`                                              | avec URL, sans, contenu long                                                    | —                                                                                        | —                                |
| `ScrollableTable`, `InlineCode`, `Markdown`, `Mermaid`, `LanguageIcon` | tableau large, code inline, Markdown riche, diagramme, erreur Mermaid           | défilement horizontal au clavier                                                         | `table` avec en-têtes            |
| `NotFoundPage`, `ErrorPage`, `DeveloperHint`                           | 404, erreur avec et sans détail                                                 | —                                                                                        | `h1`                             |
| `MdxPage`                                                              | page avec sommaire, sans, avec métadonnées, boutons « copier en Markdown »      | menu « ouvrir dans ChatGPT/Claude » (`fn`), copie                                        | `main`, `article`                |
| `Typography`, `Heading`, `MdxComponents` (TST5, après STY5)            | chaque niveau, prose complète (titres, listes, tableaux, code, citations)       | ancres des titres                                                                        | hiérarchie des titres            |

### 4.3 `Slides` (TST1), `Deck` et `Motion` (TST4a), `Kit` et `Sites` (TST4b)

| Surface | Composant                                                                                                                                                              | États                                                                       | Interactions                                                                                                      |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Slides  | les 9 existants, réécrits ; `Card`, `Columns`/`Col`, `SlideImage`, `Badge`, `Kbd`, `Step`, `SlideMotion`                                                               | chaque variante, `fit`, contenu long, image manquante                       | `CodeGroup` : onglets ; `CodeBlock` : copier ; `Mermaid` : rendu puis erreur                                      |
| Deck    | `Presentation` (dans `withDeck`)                                                                                                                                       | diapositive 1, milieu, dernière, mode `?print=1`, notes                     | flèches, Espace, `Home`/`End`, Maj+D et `c` (stylo, effacer), `s` ouvre la console (ouverture moquée), `?slide=N` |
| Deck    | `SlideStage`, `SlideFrame`, `HtmlSlide`, `SceneContent`                                                                                                                | 16:9 et 4:3, à l'échelle dans trois tailles de cadre, fond, progression     | redimensionnement du cadre (`ResizeObserver` moqué) recalcule l'échelle                                           |
| Deck    | `PresenterView`                                                                                                                                                        | diapositive courante et suivante, notes, minuteur à 0 et après 10 min       | touches de navigation, minuteur démarre et se remet à zéro                                                        |
| Deck    | `MobileRemoteView`, `RemoteQrCodeModal`, `AnnotationCanvas`, `DevHud`                                                                                                  | connecté, déconnecté, QR affiché, tracé, HUD avec diagnostics               | boutons précédent/suivant (`fn`), tracé par pointeur, effacer                                                     |
| Motion  | les 8 blocs dans `withFilm`                                                                                                                                            | chaque thème, 16:9 et 9:16, contenu minimal et maximal                      | — (état final `StaticAnim` ; le mouvement reste aux goldens)                                                      |
| Motion  | `player`                                                                                                                                                               | en pause, en lecture, fin                                                   | lecture/pause, barre de progression                                                                               |
| Kit     | `Cards`, `ExampleHeader`, `SourceFile`, `Tutorial`                                                                                                                     | 1 à 6 cartes, en-tête avec et sans badge, fichier long, tutoriel à 5 étapes | —                                                                                                                 |
| Sites   | `www/Hero`, `FormatGrid`, `OneSource`, `docs/StatusBadge`, `motion/VideoDownloads`, `components/FormatSupport`, `deck-mdx/Counter`, `Timeline`, `deck-showcase/Metric` | défaut, variantes                                                           | `Counter` : incrémente ; `Timeline` : étape active                                                                |

## 5. Conventions

### 5.1 Fichiers

- `X.stories.tsx` **à côté** de `X.tsx` ; un fichier par composant, un export par état, les
  interactions dans la story qui les concerne.
- `X.test.tsx` seulement pour ce qu'une story ne couvre pas (logique sans DOM, SSR par
  `renderToString`) ; un test qui a besoin d'un composant monté réutilise la story
  (`composeStories`) au lieu de remonter un harnais.
- Les 24 `.test.tsx` de `packages/site` qui ne font que rendre migrent vers des stories (TST3) ;
  ceux qui testent la logique (`transform-config`, `useEvent`, `processRoutes`, SSR) restent.
- Pas de dossier `__stories__` : la story suit le composant, comme le module CSS du plan 19.

### 5.2 Ce qu'une story ou un test affirme

- Des **rôles et des noms** (`getByRole("button", { name: /copy/i })`), jamais une classe, jamais un
  sélecteur CSS, jamais un texte de classe de module.
- **Aucun snapshot de DOM** (`toMatchSnapshot` sur du HTML interdit) : un snapshot ne dit pas ce qui
  compte ; le visuel est la capture, le comportement est le `play`.
- `data-testid` **interdit** sauf sur un élément sans sémantique et sans texte (un cadre de
  diapositive, un canevas), listé dans `scripts/check-stories.js` ; un `data-testid` ajouté sans
  cette entrée fait échouer le compteur.
- Une classe de module s'affirme en important `classes` dans le test et en vérifiant
  `toHaveClass(classes.open)` : le nom haché ne s'écrit jamais en dur ; cela ne sert qu'aux états
  sans rôle (`open`/`closed` d'un conteneur), et le `play` préfère `aria-expanded`.
- Les rappels sont des `fn()` d'`args` et s'affirment par `toHaveBeenCalledWith`.
- Le presse-papiers, `IntersectionObserver`, `ResizeObserver`, `matchMedia`, `localStorage` et
  `BroadcastChannel` sont moqués dans `vitest.setup.ts` de Storybook, une seule fois.
- Aucune erreur console pendant une story : le harnais échoue sur `console.error` (le React #418
  intermittent connu s'observe en e2e, pas ici : le rendu Storybook n'hydrate pas).

### 5.3 La loi du plan 19 s'applique aux stories

Une story est un `.tsx` de `packages/` ou de `sites/storybook` : le compteur
`scripts/check-style-rule.js` la lit. Pas de `style={{ display: "grid" }}` pour disposer les
variantes (`Callout.stories.tsx:26` aujourd'hui) : un décorateur `withGrid` adossé à
`decorators/grid.module.css`, ou `parameters.layout`. Les décorateurs sont des composants comme les
autres : un module chacun.

### 5.4 Nommage et langue

Titres de stories en anglais (`Site/UI/Button`, exports `Default`, `Disabled`, `Loading`,
`WithIcon`), texte des stories en Lorem Ipsum, commentaires en anglais comme le code. Les `play`
s'appellent par le comportement (`opens on click and closes on Escape`), pas par l'implémentation.

## 6. Mesure

### 6.1 `scripts/check-stories.js` (TST0, en avertissement ; TST5, en erreur)

Entré dans `check:other`. Pour chaque fichier de composant des surfaces de §3.1 (un `.tsx` qui
exporte un composant React, hors `*.test.tsx`, `*.stories.tsx`, `index.ts`, hooks, utilitaires), il
signale : (1) pas de `X.stories.tsx` à côté ; (2) une story sans `play` pour un composant
**interactif** (il reçoit `onClick`, `onChange`, `onOpenChange`, ou rend un `button`, `input`, `a`,
`dialog`, `menu`) ; (3) `data-testid` hors liste ; (4) `toMatchSnapshot` sur du HTML ; (5)
`tags: ["!test"]` sans commentaire la ligne au-dessus. Compte par surface, total, baseline
`scripts/stories-baseline.json` qui refuse toute hausse ; `--strict` à TST5. Test
`scripts/__tests__/check-stories.spec.ts`.

### 6.2 Le cliquet de couverture

`test:coverage` mesure déjà ; TST0 note la couverture par paquet dans le journal (lignes, branches)
; TST5 pose dans chaque `vitest.config.ts` de projet `coverage.thresholds` **au niveau mesuré**
(arrondi à l'unité inférieure), avec `autoUpdate: true` : la couverture ne peut que monter. Pas
d'objectif chiffré : le compteur de §6.1 dit ce qui manque, la couverture dit que rien ne régresse.

### 6.3 Ce qui est compté comme « fait »

| Indicateur                       | Avant (29/10)                        | Après TST5 (cible)                                                                |
| -------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------- |
| Fichiers de story                | 9                                    | environ 110 (27 `ui`, 45 framework, 16 slides, 10 deck, 9 motion, 4 kit, 9 sites) |
| Stories (exports)                | 16                                   | 400 à 500                                                                         |
| Stories avec `play`              | 0                                    | une par composant interactif (environ 60)                                         |
| Captures Storybook `-linux.png`  | 32                                   | 800 à 1 000 (clair et sombre)                                                     |
| Specs e2e de flux sans capture   | 2 (`deck-features`, `accessibility`) | 4 (+ `site-behaviour`, `deck-behaviour`)                                          |
| Composants sans story (compteur) | environ 120                          | 0                                                                                 |

## 7. Câblage CI et durées

| Étape                | Commande                                         | Où                                   | Durée estimée                                       |
| -------------------- | ------------------------------------------------ | ------------------------------------ | --------------------------------------------------- |
| Unitaires et stories | `pnpm test` (projets Vitest, `storybook` inclus) | `pnpm verify`, `build.yaml` « test » | + 1 à 2 min pour 400 stories dans Chromium          |
| Compteur de stories  | `node scripts/check-stories.js`                  | `pnpm check`                         | secondes                                            |
| Build Storybook      | `nx run recit-storybook:build`                   | `pnpm build` (déjà)                  | environ 1 min                                       |
| Captures Storybook   | `playwright test e2e/storybook.spec.ts`          | conteneur e2e (`-linux.png`)         | + 4 à 8 min à 1 000 captures ; par surface en vague |
| Flux                 | `playwright test e2e/*-behaviour.spec.ts`        | conteneur e2e                        | + 1 min                                             |
| Goldens              | inchangé                                         | `build.yaml`                         | inchangé                                            |

`pnpm verify` gagne 1 à 2 min ; le job `build.yaml` passe de 12–15 à 18–25 min. Si les captures
Storybook dépassent 5 min, la spec se découpe par surface (`storybook-site`, `storybook-slides`, …)
et les vagues ne rejouent que la leur, comme `20-multi-agents.md` §3.2 le prévoit pour les sites.

## 8. Lots

Chaque lot nomme ses **fichiers possédés**, ce dont il **dépend**, ce qu'il **fournit**, sa
**vérification** et sa **vague** dans le découpage de `20-multi-agents.md` §4 (les lots TST
s'intercalent dans les vagues STY : un lot TST n'entre dans une vague que si tous ses fichiers sont
libres de tout lot STY de cette vague). Tailles S, M, L. Règles de `20-multi-agents.md` §3 : un lot,
un agent, ses fichiers ; relire avant d'écrire ; jamais de git ; note `### TSTn — <titre> — <date>`
en fin de `journal.md` ; `BROWSER=none` pour tout serveur local.

### TST-A — Le filet comportemental e2e (M), vague 2

Dépend de : rien (les sites sont construits à chaque passage). Parallèle avec : STY2, STY6.1, STY7a.
Agent frais.

Fichiers : `e2e/site-behaviour.spec.ts` (nouveau), `e2e/deck-behaviour.spec.ts` (nouveau),
`e2e/support.ts` (helpers ajoutés seulement), la note de journal.

- [ ] A.1 — Site (`sites/docs` en preview) : `Ctrl+K` ouvre la recherche, saisie, flèche, Entrée
      mène à la page ; le `ThemeSwitch` passe en sombre et le rechargement le garde ; le tiroir
      mobile (projet `mobile`) s'ouvre, un lien navigue, le tiroir est fermé ; le sommaire marque la
      section visible après défilement ; le bouton copier d'un bloc de code remplit le
      presse-papiers (permission accordée au contexte) ; les onglets de `CodeTabs` changent au
      clavier ; la pagination précédent/suivant.
- [ ] A.2 — Deck (`examples/deck-starter` par `recit-site preview` d'un site qui le publie, ou le
      `present/*` de `sites/presentation`) : flèches, Espace, `Home`/`End`, `?slide=N`, `s` ouvre la
      console présentateur (`context.waitForEvent("page")`), la console suit la diapositive, mode
      `?print=1` : autant de `.print-slide` que de diapositives.
- [ ] A.3 — Aucune capture : assertions de rôle, d'URL, de `localStorage` et de presse-papiers ;
      deux passages en conteneur verts.

Fournit : la preuve comportementale qui traverse le plan 19 ; chaque vague STY rejoue ces deux specs
à sa porte.

Vérification : `pnpm test:e2e:linux e2e/site-behaviour.spec.ts e2e/deck-behaviour.spec.ts` deux fois
; `pnpm -F recit-e2e typecheck` ; `pnpm check`.

### TST0 — Le socle Storybook test (M), vague 3

Dépend de : STY6.1 (qui réécrit `.storybook/*` sans Tailwind et pose le module de `withSlide`),
STY0a (Mantine dans le framework). Parallèle avec : STY3a-c, STY6.2, STY7b — STY6.2 possède
`packages/components/src/**`, TST0 n'y touche pas. Agent **frais** avec un brief long : la vague 3 a
déjà ses trois agents lourds (STY3a-c), et §2, §3 et §1 de ce plan suffisent à décrire le harnais.

Fichiers : `sites/storybook/.storybook/main.ts` (addon, globs de §3.1, plugin des modules virtuels),
`preview.tsx` (décorateurs de §3.2 importés), `.storybook/decorators/*` (nouveau, un module CSS
chacun), `.storybook/vitest.setup.ts` (nouveau), `sites/storybook/vitest.config.ts` (nouveau),
`sites/storybook/package.json` (addon, `@recit/site` en dev, `test` script), `pnpm-workspace.yaml`
(catalogue : `@storybook/addon-vitest`, `@vitest/browser-playwright` ; Storybook aligné sur 10.6.5
si l'addon l'exige), `pnpm-lock.yaml`, `vitest.config.ts` racine (projet), `package.json` racine
(`check:other`), `sites/storybook/src/harness/**` (config de fixture, modules virtuels, index
Pagefind de fixture), `packages/site/src/lib/components/Header.stories.tsx` (la story pilote du
site, fichier nouveau que STY4a ne possède pas en vague 3), `e2e/storybook.spec.ts` (lecture de
`index.json`), `scripts/check-stories.js`, `scripts/stories-baseline.json`,
`scripts/__tests__/check-stories.spec.ts`, `.github/workflows/build.yaml` (rien si `pnpm test`
suffit ; sinon une étape), la note de journal.

- [ ] 0.1 — L'addon et le projet Vitest (§2.2) : `pnpm test --project storybook` exécute les 16
      stories existantes dans Chromium, a11y en erreur.
- [ ] 0.2 — Les décorateurs de §3.2 et les presets de viewport ; `withMantine` branché sur le thème
      de STY2.
- [ ] 0.3 — Le harnais du site : `withSite`, le plugin des modules virtuels, la story pilote
      `Site/Components/Header` avec un `play` (le lien actif, l'ouverture de la recherche) qui passe
      dans Chromium ; c'est la preuve que les stories du framework sont possibles.
- [ ] 0.4 — `storybook.spec.ts` lit `index.json` : les 16 + 1 stories ont leurs captures, baseline
      régénérée à la porte de la vague.
- [ ] 0.5 — `check-stories.js` en avertissement, baseline écrite, test ; couverture par paquet notée
      au journal (§6.2).
- [ ] 0.6 — Durée de `pnpm test` avant et après, notée.

Fournit : le runner, les décorateurs, le harnais, le compteur ; les lots TST1 à TST5 n'écrivent plus
que des stories.

Vérification : `pnpm test --project storybook` ; `pnpm test` complet ;
`pnpm -F recit-storybook build && pnpm -F recit-storybook typecheck` ;
`pnpm exec vitest run scripts/__tests__` ; `pnpm check` ;
`pnpm test:e2e:linux e2e/storybook.spec.ts` à la porte.

### TST1 — Les stories de `@recit/components` (M), vague 4

Dépend de : TST0, STY6.2. Parallèle avec : STY4a-b, STY6.3a-b, STY8, TST2a-b. Agent frais.

Fichiers : `packages/components/src/**/*.stories.tsx` (les 9 réécrits selon §3.3 et §5.3, les 6
nouveaux de §1.6), `sites/storybook/.storybook/decorators/grid.module.css` si `withGrid` n'existe
pas encore (demander à TST0 sinon), la note de journal.

- [ ] 1.1 — Chaque composant : ses états ; `play` pour `CodeGroup` (onglets), `CodeBlock` (copier),
      `Mermaid` (rendu, erreur), `Step`.
- [ ] 1.2 — `slides.spec.ts` réduit à ce que les stories ne disent pas (HTML des presets) ;
      `code-block.spec.tsx` réutilise la story par `composeStories`.
- [ ] 1.3 — Compteur à zéro sur `packages/components`.

Vérification : `pnpm test --project storybook`, `pnpm exec vitest run packages/components`,
`node scripts/check-stories.js packages/components`, captures à la porte.

### TST2 — Les stories de `Site/UI` (L, deux agents), vague 4

Dépend de : TST0, STY3a-c. Parallèle avec : STY4a-b (qui possèdent `lib/components`, pas `ui/`),
TST1. Agents frais.

- **TST2a** : `Button`, `ButtonGroup`, `Badge`, `Kbd`, `Toggle`, `Input`, `InputGroup`, `Label`,
  `Textarea`, `Separator`, `Card`, `Alert`, `Callout`, `Stepper`, `CodeBlock`, `EmbeddedCodeBlock`,
  `CodeTabs`, `CodeTabPanel`, `SyntaxHighlight` — fichiers
  `packages/site/src/lib/ui/<X>.stories.tsx`.
- **TST2b** : `Dialog`, `Drawer`, `Popover`, `Tooltip`, `DropdownMenu`, `Command`/Spotlight,
  `Collapsible`, `NavigationMenu` — idem ; les surcouches sont les `play` les plus riches (§4.1).

- [ ] 2.1 — États et `play` de §4.1 ; `CodeBlock.test.tsx` et `Stepper.test.tsx` migrés vers
      `composeStories` ou supprimés s'ils ne disent rien de plus.
- [ ] 2.2 — Compteur à zéro sur `packages/site/src/lib/ui`.

Vérification : `pnpm test --project storybook`, `pnpm exec vitest run packages/site`,
`node scripts/check-stories.js packages/site/src/lib/ui`, captures à la porte.

### TST3 — Les stories du framework (L, deux agents), vague 5

Dépend de : TST0, STY4a-b. Parallèle avec : STY5a-b (qui possèdent `Typography`, `Heading`,
`MdxComponents`, `main.css` : TST3 ne les touche pas), TST4a-b. Agents frais.

- **TST3a, pages et mise en page** : `Header`, `TopNavigation`, `HeaderNavigation`,
  `MobileTopNavigation(+Drawer)`, `Footer`, `Banner`, `PageProgress`, `Spinner`, `BrowserWindow`,
  `Framed`, `Anchor`, `Pagination`, `Search`, `DeveloperHint`, `CategoryHeading`, `NotFoundPage`,
  `ErrorPage`, `LanguageIcon`, `ScrollableTable`, `InlineCode`, `Markdown`, `Mermaid`,
  `ThemeSwitch`, `Slot` — `packages/site/src/lib/components/<X>.stories.tsx` ; les `.test.tsx` de
  rendu correspondants migrés ou supprimés (§5.1).
- **TST3b, navigation et plugins** : `lib/components/navigation/*.stories.tsx`,
  `lib/plugins/markdown/MdxPage.stories.tsx`, `search-pagefind/*.stories.tsx` (index de fixture du
  harnais), `search-inkeep` (story `!test` : widget tiers, raison écrite).

Vérification : comme TST2 sur `packages/site/src/lib/components` et `lib/plugins` ; `pnpm test`
complet ; captures à la porte.

### TST4 — Deck, motion, kit, sites (M, deux agents), vague 5

Dépend de : TST0, STY6.3a-b, STY7b, STY8. Parallèle avec : STY5a-b, TST3a-b. Agents frais.

- **TST4a, deck et motion** : `packages/formats/deck/src/**/*.stories.tsx` (`withDeck`),
  `packages/formats/motion-blocks/src/blocks/*.stories.tsx` et `player.stories.tsx` (`withFilm`),
  `sites/storybook/.storybook/decorators/deck.tsx` et `film.tsx` s'ils ne sont que des squelettes
  après TST0, la note de journal. `first-render.spec.tsx` et `presentation-a11y.spec.tsx` restent
  (SSR) ; `player.test.tsx` réutilise la story.
- **TST4b, kit, sites, exemples** : `packages/tools/site-kit/src/components/*.stories.tsx` (les six
  composants des sites y ont été déplacés : un site, c'est du Markdown et de la config),
  `examples/{deck-mdx,deck-showcase}/components/*.stories.tsx` (les globs sont dans `main.ts` depuis
  TST0 ; les exemples sont des fixtures e2e : Lorem Ipsum, rien d'autre ne bouge).

Vérification : `pnpm test --project storybook`,
`pnpm exec vitest run packages/formats/deck packages/formats/motion-blocks packages/tools/site-kit`,
`pnpm -F @recit/motion-render goldens:check` (rien ne doit bouger), compteur sur ces surfaces,
captures à la porte.

### TST5 — Clôture (M), vague 6

Dépend de : STY5a (typographie), TST1 à TST4. Parallèle avec : STY9, STY10.2 (qui possède
`CONTRIBUTING.md`, `NOTICE.md`, `sites/docs/sidebar.ts` : TST5 lui demande une ligne de sidebar et
une phrase de CONTRIBUTING). Agent frais.

Fichiers : `packages/site/src/lib/components/Typography.stories.tsx`, `Heading.stories.tsx`,
`packages/site/src/lib/util/MdxComponents.stories.tsx`, `scripts/check-stories.js` (mode erreur,
baseline supprimée), les `vitest.config.ts` de projet (seuils de couverture, §6.2),
`sites/docs/pages/documentation/testing.mdx` (nouveau : la pyramide, écrire une story, un `play`,
lancer les tests, lire la galerie), `.claude/skills/testing/SKILL.md` (nouveau : la recette d'une
story et d'un `play`, les moqueries disponibles, les interdits de §5.2, la liste de contrôle), la
note de journal.

- [x] 5.1 — Les trois stories de typographie après STY5a ; compteur à zéro partout, en erreur.
- [x] 5.2 — Seuils de couverture posés au niveau mesuré, `autoUpdate` (dans le `vitest.config.ts`
      racine, un glob par paquet : Vitest ne lit les seuils que là).
- [x] 5.3 — Docs (`documentation/reference/testing.mdx`), skill ; `AGENTS.md` reçoit une ligne «
      Tests » dans la section « Styling » ou à côté (via STY10.2, même vague : demander).
- [x] 5.4 — Durées finales de `pnpm test`, du build Storybook et des captures notées ; découpage de
      `storybook.spec.ts` par surface si > 5 min (§7) : six specs `storybook-<surface>.spec.ts`.

Vérification : `pnpm verify` ; `pnpm test:e2e:linux` complet deux fois ; `pnpm check` en mode
strict.

### 8.1 Les vagues, imbriquées dans `20-multi-agents.md` §4

| Vague (plan 20) | Lots STY de la vague     | Lots TST ajoutés                           | Agents en plus | Condition d'entrée des TST                                      |
| --------------- | ------------------------ | ------------------------------------------ | -------------- | --------------------------------------------------------------- |
| 2               | STY2, STY6.1, STY7a      | **TST-A**                                  | 1 frais        | aucune : e2e sur les sites construits                           |
| 3               | STY3a-c, STY6.2, STY7b   | **TST0**                                   | 1 frais        | STY6.1 et STY0a vérifiés                                        |
| 4               | STY4a-b, STY6.3a-b, STY8 | **TST1**, **TST2a**, **TST2b**             | 3 frais        | TST0 vérifié, STY3a-c et STY6.2 vérifiés                        |
| 5               | STY5a-b                  | **TST3a**, **TST3b**, **TST4a**, **TST4b** | 4 frais        | STY4a-b, STY6.3a-b, STY7b, STY8 vérifiés                        |
| 5'              | STY5c                    | —                                          | —              | (TST3-4 peuvent déborder ici : aucun fichier commun avec STY5c) |
| 6               | STY9, STY10.2            | **TST5**                                   | 1 frais        | STY5a, TST1 à TST4 vérifiés                                     |

Au plus **six agents** dans une vague (vague 5 : STY5a-b et TST3a-b, TST4a-b), ce qui dépasse le
plafond de cinq de `20-multi-agents.md` §4 : la vague 5 se joue en deux temps (TST3a-b avec STY5a-b,
puis TST4a-b) si la limite d'usage le demande, sans changer les dépendances. Les portes restent
celles du plan 20 §3.2 : e2e en conteneur sur un arbre stable, galerie, revue ; les captures
Storybook s'y ajoutent. Neuf lots TST, douze agents, aucun nouveau jalon humain.

### 8.2 Version plan

Les stories et les specs ne changent aucun contrat publié ; `sites/storybook` est privé. Seuls
`packages/components/package.json` (script `test`, devDependency Storybook déjà là) et les
`vitest.config.ts` des paquets (seuils) bougent : **pas de version plan** pour ce plan, sauf si
TST4a exporte une fixture de `@recit/deck` pour `withDeck` (alors `prerelease`, dans le plan du plan
19).

### 8.3 Effort

| Lot     | Taille | Stories (ordre de grandeur) | `play`      |
| ------- | ------ | --------------------------- | ----------- |
| TST-A   | M      | —                           | 12 flux e2e |
| TST0    | M      | 1 (pilote)                  | 1           |
| TST1    | M      | 40                          | 5           |
| TST2a-b | L      | 120                         | 25          |
| TST3a-b | L      | 160                         | 20          |
| TST4a-b | M + M  | 90                          | 10          |
| TST5    | M      | 20                          | 2           |

Environ 430 stories et 75 `play`, dix agents, répartis sur les vagues 2 à 6 du plan 19 : le plan 21
n'allonge pas le calendrier du plan 19, il remplit ses vagues.

## 9. Risques

| Risque                                                                                                       | Parade                                                                                                                                                               |
| ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Le harnais du site (modules virtuels, config, routeur) est trop lourd à maintenir                            | TST0 le prouve sur `Header` avant tout ; les fixtures sont un `recit.config` ordinaire ; repli : stories du framework limitées à `ui/` et aux composants sans config |
| Mode navigateur de Vitest instable (portails, focus, timing des `play`)                                      | `waitFor` et `findBy*` plutôt que `getBy*` après une interaction ; `withReducedMotion` ; `retry: 1` sur le projet `storybook` seulement                              |
| Durée de `pnpm test` et du job CI                                                                            | mesure à TST0.6 et TST5.4 ; projet `storybook` isolable (`--project`) ; captures découpées par surface                                                               |
| Chromium absent sur un poste                                                                                 | message clair de Vitest ; `pnpm exec playwright install chromium` dans CONTRIBUTING ; le conteneur e2e l'a                                                           |
| Versions : addon 10.6.5 contre Storybook 10.6.3 du catalogue, `@vitest/browser-playwright` contre Vitest 4.1 | TST0 aligne le catalogue (Storybook 10.6.5) et vérifie la matrice de versions ; une incompatibilité arrête TST0 et se note au journal                                |
| Les stories deviennent la seule doc des composants et vieillissent                                           | le compteur, l'autodoc, la capture : une story fausse échoue                                                                                                         |
| `play` écrits contre l'implémentation (classes, structure)                                                   | §5.2 ; revue de diff ; le skill `testing`                                                                                                                            |
| Le React #418 intermittent brouille les portes                                                               | il ne concerne que l'hydratation e2e ; les stories n'hydratent pas                                                                                                   |
| Doublon entre une story et un `.test.tsx` de rendu                                                           | §5.1 : le test de rendu migre ou meurt ; `composeStories` pour le reste                                                                                              |

## 10. Questions ouvertes

1. **`pnpm test` inclut-il le projet `storybook`** (proposé : oui, Chromium requis en local), ou un
   script `test:stories` séparé pour garder `pnpm verify` sans navigateur ?
2. **Les 24 `.test.tsx` de rendu de `packages/site`** : migrer vers des stories (proposé) ou les
   garder en double le temps du plan 19 ?
3. **Storybook pour les sites** (`Sites/www/Hero`…) : utile, ou leurs captures de page suffisent ?
   Proposé : oui pour les composants réutilisés par plusieurs sites (`site-kit`), au cas par cas
   pour les autres.
4. **Captures Storybook** : une par story en clair et en sombre (proposé, 800 à 1 000 images), ou
   seulement les stories marquées `parameters.screenshot` ?
5. **Seuils de couverture** : cliquet au niveau mesuré (proposé) ou un plancher commun (70 % de
   lignes) ?
6. **`withDeck`** : exporter une fixture depuis `@recit/deck` (version plan) ou compiler la fixture
   dans `sites/storybook` avec `@recit/core` (proposé, aucun contrat publié ne bouge) ?
7. **Storybook publié** : déployer `storybook-static` comme un site (`storybook.recit.dev`) avec les
   autres ? Hors de ce plan, à noter dans `07-domaines`.
