# Slidia v2 — Étape 3 : refonte visuelle — Plan d'implémentation

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal :** aligner l'interface de Slidia sur celle de Scormia et Moodia, pour qu'un utilisateur passant d'un outil à l'autre ne soit pas dépaysé — et mettre au travail les cinq composants partagés livrés à l'étape 1, qui n'ont encore aucun consommateur en production.

**Architecture :** Slidia abandonne son interface propre — hero, tuiles, filtrage client, bascule grille/liste — pour la structure en trois blocs de Scormia : en-tête d'outil, barre de filtres, grille paginée. Le filtrage passe côté serveur, avec la pagination SQL livrée à l'étape 1 et jamais branchée. Le wizard en trois étapes cède la place à une page de choix puis à un écran de configuration unique.

**Tech Stack :** Symfony 7.3, PHP 8.2+, Doctrine ORM, Twig, Tailwind CSS v4, Stimulus 3, Turbo, PHPUnit 12.

**Spec de référence :** `docs/specs/2026-08-01-slidia-v3-refonte-visuelle-design.md`

## Global Constraints

- Branche de travail : `slidia_v2`. Ne pas merger.
- **Ne jamais utiliser `git add -A`, `git add .` ni `git commit -a`.** Stager nommément les fichiers de sa tâche.
- **Scormia ne doit pas être modifié, d'un seul octet** : ni `src/Controller/Scormia/`, ni `src/Repository/Scormia/`, ni `src/Service/Scormia/`, ni `templates/scormia/`, ni `assets/controllers/scormia/`. C'est la référence de design, on la lit.
- **Le contrat DOM du partage public doit survivre** — six points, détaillés ci-dessous. Il se casse en silence.
- **Le format persisté de `slidesData` reste inchangé** : `{layoutPath, fields, notes}`.
- Tout nouveau fichier PHP commence par `declare(strict_types=1);`.
- Commentaires, messages de commit, noms de tests et vocabulaire métier en **français**.
- **Aucun test ne doit appeler l'API OpenAI** : doubler `OpenAIClientInterface`.
- `npm run build` doit compiler. `public/build/` est ignoré par git, n'y touchez pas.
- **`make:migration` est inexploitable** : la base de développement porte des migrations absentes de la branche, et sa sortie contient des `DROP COLUMN` sur des colonnes réelles. Toute migration s'écrit à la main, sur le modèle de `migrations/Version20260801110000.php`, et **ne s'exécute pas**.
- Tests : `php bin/phpunit tests/Unit/` et `php -d memory_limit=1G bin/phpunit tests/Functional/`. Références au départ : **2261 unitaires, 534 fonctionnels**.

## Le contrat du partage public

Le travail de partage public a été fusionné deux fois dans cette branche. Six points de contrat DOM que la refonte peut casser sans qu'aucun test ne bronche :

1. la classe **`sdl-pres-card`** sur le conteneur de carte ;
2. les attributs **`data-uuid`**, **`data-shared`**, **`data-share-download`**, **`data-share-url`** sur ce conteneur ;
3. l'élément du **tag « Partagé » toujours rendu**, masqué par `style="display:none"` — le rendre conditionnellement en Twig casserait l'activation à chaud ;
4. la recopie du jeu de données dans **`toggleMenu()`**, en attributs bruts (`dataset`) et **non** en paramètres Stimulus — un paramètre convertirait `"1"` en nombre et casserait les comparaisons ;
5. la **réinitialisation du partage** lors d'une duplication : le clone reçoit le nouvel identifiant et `data-shared="0"` ;
6. l'**unicité** de la modale de partage sur la page.

La raison : le bouton « Partager » vit dans un menu contextuel global, jamais descendant d'une carte. Le contrôleur de partage retrouve la carte par `document.querySelector('.sdl-pres-card[data-uuid="…"]')`.

**Degré de liberté** : la modale est résolue par sélecteur global — elle peut être déplacée hors du contrôleur de liste.

Avant de committer une tâche qui touche la carte, l'accueil ou le contrôleur de liste, lancer :

```bash
php -d memory_limit=1G bin/phpunit tests/Functional/Controller/Slidia/SlidiaShareUiTest.php tests/Functional/Controller/Slidia/SlidiaPublicShareTest.php --testdox
```

---

## Structure des fichiers

| Fichier | Responsabilité |
|---|---|
| `src/Entity/SlidiaCategory.php`, `src/Repository/SlidiaCategoryRepository.php` | *(supprimés)* |
| `src/Entity/SlidiaPresentation.php` | *(modifié)* Retrait de `category` et `pinned` |
| `migrations/Version*.php` | *(créé)* Suppression de la table et des deux colonnes |
| `src/Controller/Slidia/SlidiaController.php` | *(modifié)* `index()` en serveur, `/new`, `/configure/{method}` |
| `src/Controller/Slidia/SlidiaApiController.php` | *(modifié)* Retrait des trois routes de catégorie |
| `src/Repository/SlidiaPresentationRepository.php` | *(modifié)* Filtre « partagées » |
| `templates/slidia/index.html.twig` | *(réécrit)* |
| `templates/slidia/_presentation_card.html.twig` | *(réécrit)* Sur `entity-card` |
| `templates/slidia/new.html.twig` | *(créé)* Sur `choice-page` |
| `templates/slidia/configure.html.twig` | *(créé)* |
| `templates/slidia/create.html.twig` | *(supprimé)* |
| `assets/controllers/slidia/list_controller.js` | *(fortement réduit)* |
| `assets/controllers/slidia/create_controller.js` | *(remplacé par `configure_controller.js`)* |
| `templates/moodia/dashboard.html.twig` | *(modifié)* Barre de recherche |

---

## Task 1 — supprimer les catégories et l'épinglage

**Files:**
- Delete: `src/Entity/SlidiaCategory.php`, `src/Repository/SlidiaCategoryRepository.php`
- Modify: `src/Entity/SlidiaPresentation.php`, `src/Controller/Slidia/SlidiaApiController.php`, `src/Controller/Slidia/SlidiaController.php`
- Create: `migrations/Version*.php`
- Test: les tests existants qui référencent ces notions

**Interfaces:**
- Produces : `SlidiaPresentation` sans `category` ni `pinned` ; les routes `api_slidia_list_categories`, `api_slidia_create_category`, `api_slidia_delete_category` supprimées.

**C'est une suppression destructive, décidée par le propriétaire du projet.** La table `slidia_category` et les colonnes `category_id` et `pinned` disparaissent. Des catégories créées par des utilisateurs seront perdues à l'application de la migration.

- [ ] **Étape 1 : recenser avant de supprimer**

```bash
grep -rn "SlidiaCategory\|categoryId\|category_id\|setPinned\|isPinned\|->pinned" src/ templates/ assets/ tests/ | grep -iv scormia
```

Consigner la liste complète dans le rapport. Toute référence non traitée fera échouer la compilation du conteneur ou un test.

- [ ] **Étape 2 : écrire les tests qui échouent**

Les tests existants portant sur les catégories ou l'épinglage doivent être **supprimés**, pas adaptés : le comportement disparaît. Ajouter en revanche un test vérifiant que les trois routes d'API de catégorie renvoient 404, et un test d'entité vérifiant que les accesseurs ont disparu.

- [ ] **Étape 3 : supprimer**

Entité, repository, relation, accesseurs, routes, et tout ce que le recensement a fait remonter. `templates/slidia/index.html.twig` sera réécrit à la tâche 4 : ici, se contenter de retirer ce qui casserait le rendu.

- [ ] **Étape 4 : écrire la migration à la main**

Un `DROP TABLE slidia_category` et un `ALTER TABLE slidia_presentation DROP category_id, DROP pinned`, plus le retrait de la contrainte de clé étrangère. Le `down()` les recrée. Docblock expliquant pourquoi elle est manuelle **et** qu'elle est destructive.

Ne pas l'exécuter.

- [ ] **Étape 5 : vérifier et committer**

```bash
php bin/phpunit tests/Unit/ --testdox
php -d memory_limit=1G bin/phpunit tests/Functional/ --testdox
php bin/console lint:container
```

---

## Task 2 — le filtre « partagées » en repository

**Files:**
- Modify: `src/Repository/SlidiaPresentationRepository.php`
- Test: `tests/Unit/Repository/SlidiaPresentationRepositoryTest.php`

**Interfaces:**
- Produces : `findByUserPaginated()` et `countByUser()` gagnent un paramètre `?bool $sharedOnly = null` — `null` pour tous, `true` pour les seules présentations partagées.

Le repository porte déjà `buildUserQuery()` et le trait de recherche. Une présentation est partagée quand son jeton de partage n'est pas nul — va vérifier la forme exacte dans `ShareableTrait`.

Scormia a déjà ce filtre côté serveur : va lire comment il l'exprime, sans modifier son code.

- [ ] **Étape 1 : écrire le test qui échoue** — le filtre produit la bonne condition DQL, et son absence n'ajoute aucune condition. Suivre le style des tests existants du fichier, qui inspectent le DQL produit.
- [ ] **Étape 2 : lancer, vérifier l'échec, implémenter, vérifier, committer**

---

## Task 3 — l'accueil côté serveur

**Files:**
- Modify: `src/Controller/Slidia/SlidiaController.php` (méthode `index()`)
- Test: `tests/Functional/Controller/Slidia/SlidiaIndexTest.php` *(créer)*

**Interfaces:**
- Produces : `index()` passe au template `presentations`, `total`, `currentPage`, `totalPages`, `search`, `templateFilter`, `sharedFilter`, `templates`, `templatesJson`, `sharedCount`, `medianTokenCost`, `canCreate`, `slidiaMinTokens`.

`index()` charge aujourd'hui **toutes** les présentations et laisse le JavaScript filtrer. Elle doit désormais faire ce que fait `ScormiaController::index()` — va la lire :

- lire `?q=`, `?template=`, `?shared=`, `?page=` ;
- compter puis paginer en SQL, avec `AppLimits::TOOL_LIBRARY_PER_PAGE` ;
- borner la page : `max(1, min($totalPages, $page))` ;
- **compteurs d'onglets absolus**, indépendants de la recherche.

Le filtre par masque accepte trois familles de valeurs : vide, l'identifiant d'un masque, ou la constante désignant l'absence de masque.

Tout le calcul en PHP sur la collection complète disparaît — partition épinglés/autres, compteurs par catégorie, statistiques.

- [ ] **Étape 1 : écrire les tests qui échouent**

Créer le fichier de test. Couvrir : la première page affiche au plus le nombre attendu ; la deuxième page affiche d'autres présentations ; une page hors bornes est ramenée dans les bornes ; le filtre par masque restreint ; le filtre « partagées » restreint ; la recherche filtre sur le titre ; les compteurs d'onglets ne suivent pas la recherche.

Les fixtures doivent contenir assez de présentations Slidia pour exercer la pagination — vérifier ce que `TestFixtures` fournit et compléter si nécessaire, comme cela a été fait pour Moodia.

- [ ] **Étape 2 : lancer, vérifier l'échec, implémenter, vérifier, committer**

---

## Task 4 — la carte sur `entity-card`

**Files:**
- Modify: `templates/slidia/_presentation_card.html.twig`
- Test: `tests/Functional/Controller/Slidia/SlidiaShareUiTest.php` (ne pas modifier, doit continuer de passer)

**C'est la tâche la plus risquée du plan** : elle touche le contrat du partage. Relire les six points en tête de plan avant d'écrire une ligne.

La carte est réécrite avec `{% embed '_organisms/entity-card/entity-card.html.twig' %}`, en supprimant :
- le thumbnail décoratif — la pile de trois slides factices ;
- les neuf variantes `[.sd-list_&]:` du mode liste ;
- la puce de catégorie.

Et en conservant, dans les blocs du composant : le titre, le nombre de slides et la date, la puce du masque, la consommation en tokens et en énergie, le menu contextuel, et le tag « Partagé ».

**Le paramètre `attr` d'`entity-card`** porte les attributs de données du conteneur : c'est par lui que passent `data-uuid`, `data-shared`, `data-share-download`, `data-share-url` et les attributs de filtrage. Le paramètre `class` doit porter `sdl-pres-card`.

- [ ] **Étape 1 : relire le contrat et le test qui le garde**

Lire `tests/Functional/Controller/Slidia/SlidiaShareUiTest.php` et `assets/controllers/shared/share_link_controller.js`, méthode privée de résolution de carte. Consigner dans le rapport ce que chacun exige du DOM.

- [ ] **Étape 2 : réécrire la carte**

- [ ] **Étape 3 : vérifier que le partage tient**

```bash
php -d memory_limit=1G bin/phpunit tests/Functional/Controller/Slidia/ --testdox
```

Ces tests ne doivent **pas** être modifiés. Si l'un échoue, c'est la carte qui est en tort.

- [ ] **Étape 4 : committer**

---

## Task 5 — l'accueil sur les composants partagés

**Files:**
- Modify: `templates/slidia/index.html.twig`
- Modify: `assets/styles/pages/slidia/index.css`

La page est réécrite autour de trois blocs, dans l'ordre de Scormia — va lire `templates/scormia/index.html.twig`, c'est le modèle :

1. `{% embed '_organisms/tool-header/tool-header.html.twig' %}` avec le bloc `actions` surchargé pour porter deux boutons : « Mes masques » en secondaire, « Créer une présentation » en principal.
2. `{% embed '_organisms/tool-toolbar/tool-toolbar.html.twig' %}` avec le bloc `leading` surchargé pour porter les onglets **et** le filtre par masque, et le paramètre `search` pour la recherche.
3. La grille `grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 lg:gap-8 mb-10`, puis `_molecules/pagination` en `variant: 'moodia'`, puis le message « aucun résultat ».

Disparaissent : le hero, les tuiles d'actions rapides, les sections épinglés/autres, le filtre de catégorie, la bascule grille/liste, l'état vide maison — remplacé par la molécule `empty-state`, comme Moodia.

**Les couleurs.** Scormia recolore les composants partagés en surchargeant des variables CSS sur un conteneur, avec un `<style>` local. Slidia fait de même, en ambre (`#F59E0B`). Reprendre le motif de Scormia, pas en inventer un autre.

**Propager les paramètres de requête** sur les liens d'onglets, sur le filtre de masque et sur la pagination — sinon un clic perd la recherche en cours. Scormia le fait avec un `{% set qp %}`.

Le `main_class` vidé disparaît : la page revient dans le gabarit applicatif, avec le conteneur `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8` des deux autres outils.

**La modale de partage** doit rester unique sur la page, et peut être placée hors du contrôleur de liste.

- [ ] **Étape 1 : réécrire le template**
- [ ] **Étape 2 : nettoyer le CSS** — retirer `.sd-grid`, `.sd-list`, `.sdl-search-bar` et ce qui ne sert plus.
- [ ] **Étape 3 : vérifier**

```bash
php bin/console lint:twig templates/slidia/
php -d memory_limit=1G bin/phpunit tests/Functional/Controller/Slidia/ --testdox
npm run build
```

- [ ] **Étape 4 : committer**

---

## Task 6 — dégraisser le contrôleur de liste

**Files:**
- Modify: `assets/controllers/slidia/list_controller.js`

Le fichier fait 1 594 lignes. Doivent disparaître :

- **le filtrage client** : `filterSearch`, `#applyFilters`, `#updateUrlParams`, et tout ce qui masque ou affiche des cartes — le serveur s'en charge ;
- **les catégories** : quatre actions publiques, sept méthodes privées, la modale ;
- **la bascule grille/liste** : `setGridView`, `setListView`, `#applyView`, le stockage local ;
- **l'épinglage** : `openPinModal` ;
- **le filtre de catégorie** : `toggleCategoryDropdown`, `selectCategoryFilter`, `_updateCategoryLabel` ;
- **le code mort recensé** : l'action `setFilter`, les targets `filterChip`, `colorPreviewBtn`, `librarySelectedLabel`, `uploadIcon`, `libraryTitleInput`.

Doivent rester : le menu contextuel, le renommage, la duplication, la suppression, le relais vers la modale de partage, et toute la bibliothèque de masques (traitée à la tâche 7).

Le filtre par masque et les onglets deviennent des liens serveur : plus aucune action Stimulus.

**Attention aux deux points de contrat** : la recopie du jeu de données dans `toggleMenu()` et la réinitialisation du partage dans la duplication. Les deux doivent survivre à l'identique.

- [ ] **Étape 1 : supprimer, en vérifiant à chaque retrait qu'aucun template ne référence l'action**
- [ ] **Étape 2 : vérifier** — `npm run build`, puis les tests fonctionnels Slidia, dont ceux du partage.
- [ ] **Étape 3 : committer**, en indiquant le nombre de lignes avant et après dans le message.

---

## Task 7 — la modale de masques, simplifiée

**Files:**
- Create: `templates/slidia/_library_modal.html.twig`, `assets/controllers/slidia/template_library_controller.js`
- Modify: `templates/slidia/index.html.twig`, `assets/controllers/slidia/list_controller.js`, `assets/controllers.json`

**Décision prise avant exécution, après inventaire des mécanismes de la modale.** Le plan prévoyait d'y tailler : réévaluer l'aperçu à flèches, les onglets mobiles, la bande de navigation, et faire rendre la grille de masques par Twig. L'inventaire a renversé les deux propositions.

Aucun mécanisme n'est décoratif : le dépôt de `.pptx`, les trois pastilles de filtre, la grille, l'aperçu d'une mise en page avec ses zones de contenu, les flèches de navigation entre mises en page, la bascule d'exclusion pour l'IA — avec son garde-fou qui interdit de tout désactiver —, le renommage, la suppression, et les onglets mobiles qui donnent accès à la seconde colonne sur petit écran. **Les masques sont ce que Slidia a et que ses voisins n'ont pas**, et le propriétaire du projet a demandé cette surface de gestion nommément. On ne retire rien.

Et la grille ne peut pas passer en Twig : elle se remet à jour **sans fermer la modale** — au changement de filtre, après un import, un renommage, une suppression. La rendre en Twig imposerait de maintenir le même markup à deux endroits. Le serveur connaît les masques, mais ce n'est pas lui qui les réaffiche.

Le vrai défaut n'est donc pas le contenu de la modale, c'est **où elle vit** : 196 lignes dans un template déjà long, et environ 46 % d'un contrôleur Stimulus qui a une tout autre responsabilité. La tâche extrait, elle ne rogne pas.

- [ ] **Étape 1 : extraire le markup** dans `templates/slidia/_library_modal.html.twig`, inclus par `index.html.twig`. Les trois modales satellites — renommage, suppression, et la modale elle-même — suivent le partial.

- [ ] **Étape 2 : extraire le JavaScript** dans un contrôleur Stimulus dédié, `slidia--template-library`, monté sur le partial. Le bouton « Mes masques » vit dans l'en-tête, **hors** de la modale : le relier par un outlet Stimulus ou par un événement, pas en dupliquant l'état. Enregistrer le contrôleur dans `assets/controllers.json`.

- [ ] **Étape 3 : retirer les deux targets morts** `libraryLayoutTitle` et `libraryMainSlide` — déclarés et rendus, jamais lus.

- [ ] **Étape 4 : vérifier** — l'import, le filtre, l'aperçu, les flèches, l'exclusion, le renommage et la suppression doivent tous fonctionner. Aucun test n'exécute de JavaScript : décrire dans le rapport comment chacun a été vérifié, action par action et target par target, dans les deux sens.

- [ ] **Étape 5 : committer**, en indiquant le nombre de lignes avant et après pour les deux fichiers d'origine.

---

## Task 8 — la page de choix

**Files:**
- Create: `templates/slidia/new.html.twig`
- Modify: `src/Controller/Slidia/SlidiaController.php`
- Test: `tests/Functional/Controller/Slidia/SlidiaNewTest.php`

**Interfaces:**
- Produces : route `GET /slidia/new`, nom `app_slidia_new`.

Rend `choice-page` avec trois cartes : importer un document, générer avec l'IA, rédiger manuellement. Les deux premières pointent vers `/slidia/configure/{method}`, la troisième crée directement une présentation vide et ouvre l'éditeur, sans consommer de tokens.

Le verrou de tokens s'applique aux deux premières, pas à la troisième — regarder comment `MoodiaController::newCourse()` et `ScormiaController::newModule()` le font.

- [ ] **Étape 1 : écrire le test qui échoue** — la page rend trois cartes ; le verrou apparaît sous le seuil de tokens ; la troisième carte reste accessible.
- [ ] **Étape 2 : lancer, vérifier l'échec, implémenter, vérifier, committer**

---

## Task 9 — l'écran de configuration

**Files:**
- Create: `templates/slidia/configure.html.twig`, `assets/controllers/slidia/configure_controller.js`
- Modify: `src/Controller/Slidia/SlidiaController.php`
- Test: `tests/Functional/Controller/Slidia/SlidiaConfigureTest.php`

**Interfaces:**
- Produces : route `GET /slidia/configure/{method}`, nom `app_slidia_configure`, avec `method` valant `pdf` ou `prompt`.

Un seul écran portant : le titre, le choix du masque, selon la méthode le dépôt du document ou la saisie du brief, le bandeau de structure détectée avec son interrupteur, et le **profil de génération**, exposé pour la première fois.

**Le contrat serveur à honorer, déjà livré et testé :**
- `POST /api/slidia/document` renvoie `{hash, sectionCount, charCount, hasStructure}` ;
- la soumission poste `title`, `templateId`, `brief`, `sourceHash`, `fidelityMode`, plus désormais `generationProfile`, vers `app_slidia_create_submit` ;
- l'interrupteur du mode fidèle **ne doit jamais porter `checked` en dur** — c'est un bug corrigé à l'étape 2b, qui faisait basculer en transcription des présentations sans document. Le commentaire qui l'explique se trouve dans le template provisoire : le reprendre.

Le choix du masque réutilise `_molecules/slidia-template-card`, déjà en place.

Le profil est exposé en langage métier — « Standard » et « Approfondi » — avec la mention que le second consomme davantage de tokens. Jamais un nom de modèle.

**Le brief en présence d'un document** — report de l'étape 2b : aujourd'hui il est silencieusement ignoré. Trancher, et l'appliquer : soit le champ est visiblement désactivé quand un document est déposé, soit le brief devient une consigne de cadrage réellement transmise. **La seconde option demande du travail côté service** : si elle est retenue, le dire et la traiter ; sinon, désactiver le champ visiblement.

- [ ] **Étape 1 : écrire les tests qui échouent** — chaque méthode rend son écran ; la soumission crée une présentation portant le profil choisi ; une méthode inconnue renvoie 404.
- [ ] **Étape 2 : lancer, vérifier l'échec, implémenter, vérifier, committer**

---

## Task 10 — retirer le wizard

**Files:**
- Delete: `templates/slidia/create.html.twig`, `assets/controllers/slidia/create_controller.js`
- Modify: `src/Controller/Slidia/SlidiaController.php`

La route `app_slidia_create` en `GET` disparaît au profit de `app_slidia_new`. La route de soumission reste : c'est elle que l'écran de configuration appelle.

Vérifier qu'aucun lien ne pointe plus vers l'ancienne route :

```bash
grep -rn "app_slidia_create\b" src/ templates/ assets/ tests/
```

- [ ] **Étape 1 : recenser les références, supprimer, vérifier, committer**

---

## Task 11 — la recherche Moodia

**Files:**
- Modify: `templates/moodia/dashboard.html.twig`
- Test: `tests/Functional/Controller/MoodiaTest.php`

Le contrôleur est prêt depuis l'étape 1 : il lit `?q=`, pagine en SQL et transmet `search` au template, avec un commentaire annonçant cette étape. Il ne manque que le markup.

Remplacer le bloc qui ne contient aujourd'hui que les onglets par la ligne à deux zones de Scormia — l'adopter via `tool-toolbar` plutôt que de recopier le markup, puisque c'est exactement ce pour quoi le composant existe.

Trois précautions, qui ne se voient pas dans le template mais casseraient la fonctionnalité :

1. **propager le paramètre de recherche** sur les liens d'onglets **et** sur la pagination ;
2. **ajouter un message « aucun résultat »**, qui n'existe pas côté Moodia — sa branche vide est gouvernée par un compteur non filtré, donc une recherche infructueuse affiche une grille vide et rien d'autre ;
3. utiliser le contrôleur Stimulus **partagé**, pas celui de Scormia.

- [ ] **Étape 1 : écrire les tests qui échouent** — une recherche filtre ; une recherche infructueuse affiche le message ; un clic d'onglet conserve la recherche ; la pagination conserve la recherche.
- [ ] **Étape 2 : lancer, vérifier l'échec, implémenter, vérifier, committer**

---

## Task 12 — les deux reports de l'étape 2b

**Files:**
- Modify: `src/Service/Slidia/SlidiaAiService.php`, `src/Controller/Slidia/SlidiaController.php`
- Test: `tests/Unit/Service/Slidia/SlidiaAiServiceTest.php`

**Le nombre de slides visé est discontinu autour du seuil de sommaire.** Un document de 49 000 caractères vise 60 slides ; un de 60 000, dix à vingt. La raison : le nombre visé se calcule sur le texte transmis au plan, qui est le document entier en deçà du seuil et le sommaire au-delà. **Le plus gros document donne la plus courte présentation.**

Correctif : transmettre explicitement à `generatePlan()` le volume réel du document, et calculer le nombre visé là-dessus.

**Le brief est ignoré en présence d'un document** — traité à la tâche 9 côté interface. Si la voie retenue est de transmettre le brief comme consigne de cadrage, c'est ici que le service doit l'accepter.

- [ ] **Étape 1 : écrire les tests qui échouent** — deux documents de part et d'autre du seuil visent un nombre de slides comparable.
- [ ] **Étape 2 : lancer, vérifier l'échec, implémenter, vérifier, committer**

---

## Task 13 — vérification de bout en bout

**Files:** aucun

- [ ] **Étape 1 : suites complètes**

```bash
php bin/phpunit tests/Unit/ --testdox
php -d memory_limit=1G bin/phpunit tests/Functional/ --testdox
```

- [ ] **Étape 2 : linters et compilation**

```bash
php bin/console lint:container
php bin/console lint:twig templates/
npm run build
```

- [ ] **Étape 3 : Scormia intact**

```bash
git diff --name-only ce9e3498..HEAD | grep -i scormia
```

Attendu : **aucun résultat**.

- [ ] **Étape 4 : le contrat du partage**

Relire les six points en tête de plan et vérifier chacun dans le DOM rendu. Les tests du partage doivent passer sans avoir été modifiés.

- [ ] **Étape 5 : plus aucune trace du supprimé**

```bash
grep -rn "SlidiaCategory\|sd-list\|setGridView\|isPinned\|app_slidia_create\b" src/ templates/ assets/ tests/ | grep -iv scormia
```

- [ ] **Étape 6 : les composants partagés sont utilisés**

```bash
grep -rln "tool-header\|tool-toolbar\|choice-page\|entity-card\|search-input" templates/slidia templates/moodia
```

Attendu : les cinq composants ont désormais au moins un consommateur en production.

- [ ] **Étape 7 : commit final**

```bash
git commit --allow-empty -m "chore(slidia): refonte visuelle de l'étape 3 vérifiée de bout en bout"
```

---

## Ce que cette étape laisse ouvert

- La purge de `var/uploads`, à décider avec le propriétaire : Slidia ne supprime jamais les PDF déposés, contrairement à Moodia.
- Les migrations en attente d'application, désormais au nombre de quatre.
- La migration de Scormia vers les composants partagés, à rendu strictement identique, si le propriétaire le souhaite un jour.
