# Tutoriels interactifs

Un tutoriel guide l'utilisateur à travers un écran : un voile sombre, une bulle par étape, et
un trou découpé autour de l'élément dont on parle. Il se lance tout seul à la première visite,
et le bouton `?` en bas à droite le rejoue à la demande.

**Un fichier par tutoriel.** Ajouter un tutoriel, c'est créer un fichier ; le retirer, c'est le
supprimer. Il n'y a aucune liste centrale à tenir à jour.

---

## Ajouter un tutoriel — moins de dix minutes

### 1. Le fichier de définition

Créez `src/Service/Tour/Tours/<Nom>Tour.php`. Un squelette existe déjà pour chaque outil de la
plateforme : ouvrez-le et remplissez `steps()`.

```php
final class PixiaTour implements TourInterface
{
    public function key(): string      { return 'pixia_studio'; }   // UNE CLÉ PAR ÉCRAN
    public function toolCode(): string { return 'pixia'; }          // → couleur de l'outil
    public function routes(): array    { return ['app_pixia']; }    // noms de routes Symfony
    public function version(): int     { return 1; }

    public function steps(): array
    {
        return [
            [
                'target'   => 'pixia-prompt',   // = data-tour-step dans le gabarit
                'position' => 'top',            // top | right | bottom | left (facultatif)
                'align'    => 'center',          // start | center | end (facultatif)
                'title'    => 'Décrivez votre image',
                // `<strong>`, `<em>` et `<br>` sont admis : voir « Mettre en forme » plus bas.
                'body'     => "Indiquez le <strong>sujet</strong>.<br><br>Le bouton <em>Améliorer</em> complète.",
            ],
        ];
    }
}
```

Rien à déclarer ailleurs : `TourInterface` porte son propre `#[AutoconfigureTag]`, le conteneur
ramasse la classe au prochain vidage de cache.

### 2. Les ancres dans le gabarit

Sur chaque élément visé, posez l'attribut :

```twig
<div class="…" data-tour-step="pixia-prompt">
```

**Uniquement cet attribut.** Jamais une classe CSS ni un sélecteur : une classe change au
premier coup de peinture, un attribut dédié se voit et se respecte. Une cible absente fait
sauter son étape en silence — aucune erreur, le tutoriel continue.

### 3. Vérifier

```bash
npm run build          # seulement si vous avez touché au JS ou au CSS
php bin/console cache:clear
```

Videz la ligne de progression pour rejouer le lancement automatique :

```sql
DELETE FROM user_tour_progress WHERE tour_key = 'pixia_studio';
```

Puis ouvrez la page. Le tutoriel doit partir seul après un demi-seconde.

---

## Mettre en forme le corps d'une étape

Trois balises sont admises, et trois seulement :

| Balise | Usage |
|---|---|
| `<strong>` | Le nom d'un bouton, un mot-clé, une valeur. S'affiche en gras encre. |
| `<em>` | Le libellé exact d'un bouton à cliquer. S'affiche en gras, à la couleur de l'outil. |
| `<br>` | Un retour à la ligne. `<br><br>` sépare deux idées avec un léger interligne. |

Le texte est inséré en `innerHTML` par la bibliothèque. **C'est sans danger parce que ces
chaînes sont écrites dans le code**, jamais saisies par un utilisateur. Le jour où un tutoriel
serait alimenté depuis la base, il faudrait les échapper.

Un corps se lit debout, entre deux clics : deux blocs séparés par un `<br><br>`, pas plus.

## Modifier un tutoriel

Corrigez le texte dans `steps()` et rechargez : rien à reconstruire, les étapes viennent du
serveur. **N'augmentez pas la version pour une faute de frappe** — cela rouvrirait le tutoriel
chez tous ceux qui l'ont déjà suivi.

## Reproposer un tutoriel à tout le monde

Augmentez `version()` :

```php
public function version(): int { return 2; }
```

Tous les utilisateurs dont la version vue est inférieure le reverront une fois. À faire quand
l'écran a changé au point que les explications sont périmées — pas autrement.

## Supprimer un tutoriel

Supprimez son fichier dans `Tours/`. Les lignes `user_tour_progress` qui portent son ancienne
clé deviennent inertes ; on peut les nettoyer, rien ne l'exige. Les `data-tour-step` du gabarit
peuvent rester : ce sont des attributs sans style ni comportement.

---

## Comment ça marche

```
src/Service/Tour/
├── TourInterface.php          contrat + #[AutoconfigureTag]
├── TourRegistry.php           inventaire : par clé, par route ; refuse les doublons
├── TourProgressManager.php    isPending / markCompleted / markSkipped / reset
└── Tours/*.php                un fichier par tutoriel

src/Entity/UserTourProgress.php     user · tourKey · status · tourVersion · completedAt
src/Controller/TourController.php   POST /tutoriels/{tourKey}/termine · /passe → 204
src/Twig/TourExtension.php          tour_courant()

templates/_organisms/tour/tour-live.html.twig   hôte + bouton, inclus 1× dans base.html.twig
assets/controllers/shared/tour_controller.js    le contrôleur unique
assets/styles/components/tour.css               habillage Bubul + bouton flottant
```

**C'est le serveur qui décide.** `tour_courant()` rend le tutoriel de la route courante avec un
drapeau `pending` ; le contrôleur Stimulus obéit. Aucun appel d'API au chargement, aucune
décision côté navigateur, et donc rien à contourner depuis la console.

**La base est la source de vérité**, pas le navigateur : un tutoriel vu au bureau ne se rejoue
pas sur le téléphone le soir. Il n'y a délibérément aucun cache en `localStorage` — il ne dirait
rien de plus que la table, et finirait par la contredire.

**Le statut « passé » est une valeur à part**, et non l'absence de ligne : on ne relance plus un
tutoriel écarté, mais on garde la trace qu'il a été proposé. Le bouton `?`, lui, ne consulte pas
cette règle : il rejoue toujours.

**Un rejeu ne déclasse pas une progression** : sortir d'un tutoriel qu'on avait suivi jusqu'au
bout n'est pas un refus, seul le lancement automatique enregistre un abandon.

### Bibliothèque

[Driver.js](https://driverjs.com) 1.8, **MIT**. Intro.js a été écarté pour sa licence : il est
en AGPL-3.0, incompatible avec un service privé payant sans licence commerciale.

Driver.js apportait déjà l'essentiel : cible absente sautée sans erreur (`skipMissingElement`),
attente d'un élément monté plus tard (`waitForElement`), indicateur d'étape, défilement doux,
pilotage au clavier (flèches, Entrée, Échap).

### Détails qui ont une raison

- **Pas d'écouteur `turbo:load`.** `connect()` de Stimulus se déclenche déjà après une
  navigation Turbo ; les deux ensemble lanceraient le tutoriel deux fois.
- **`data-turbo-temporary` sur l'hôte.** Sans lui, Turbo restaure l'hôte depuis son instantané
  avec le `pending` d'alors, et `connect()` relance un tutoriel qu'on vient de terminer.
- **La marque anti-relance est portée par le DOM** (`data-tour-joue`), pas par l'instance : elle
  survit à une reconnexion de Stimulus.
- **L'overlay est nettoyé dans `disconnect()`.** Sans cela, une navigation Turbo pendant un
  tutoriel laisse un écran assombri que plus rien ne peut fermer.
- **L'élément mis en avant reste cliquable** (`disableActiveInteraction: false`), mais un clic
  dessus ne fait pas avancer (`advanceOnClick: false`) : le tutoriel explique, il ne prend pas
  la main.
- **`z-index` 700 / 701.** Le plafond de l'interface authentifiée est 600 (l'overlay plein écran
  de l'éditeur Slidia).
- **Le bouton flottant est en `z-index` 40**, sous les toasts (9999) qui occupent le même coin
  mais ne font que passer. Une page qui a déjà du mobilier en bas à droite le pousse vers le
  haut en déclarant `<style>:root { --tour-bouton-decalage: 60px; }</style>` — l'éditeur Moodia
  le fait pour sa barre de navigation fixe. **Sur `:root`, pas sur un conteneur parent** :
  l'hôte du tutoriel est inclus depuis `base.html.twig`, dans un autre sous-arbre du document,
  et une variable CSS ne remonte pas.

### Ce qui n'est pas couvert par les tests automatiques

Rien dans le dépôt ne pilote Stimulus ni le DOM. Sont testés : `TourRegistry`,
`TourProgressManager` et les deux points d'écriture (`tests/Unit/Service/Tour/`,
`tests/Functional/Controller/TourControllerTest.php`). Le déroulé dans le navigateur — voile,
bulles, clavier, rejeu — se vérifie à la main.

### Désinstaller la fonctionnalité

```bash
rm -r src/Service/Tour src/Entity/UserTourProgress.php src/Repository/UserTourProgressRepository.php \
      src/Controller/TourController.php src/Twig/TourExtension.php src/Config/TourConfig.php \
      templates/_organisms/tour assets/controllers/shared/tour_controller.js \
      assets/styles/components/tour.css docs/tours.md \
      tests/Unit/Service/Tour tests/Functional/Controller/TourControllerTest.php
npm rm driver.js
```

Puis retirer une ligne de `templates/base.html.twig`, une de `assets/styles/app.css`, et générer
une migration de suppression de table. Aucune autre partie du code ne pointe vers `Tour`.
