# Story: Inline Editor — Dynamic Block Add/Remove/Reorder, Mobile Image Toggle, Tag Picker

## 1. Contexte (Context)

**There is no existing precedent in this codebase for add/remove/reorder block UI in the inline editor.** `assets/admin/editor/ContentSerializer.ts`'s `serializeSectioned()` today only walks existing, server-rendered `[data-section]` DOM elements at page-load time — it has no logic for a client-mutated, arbitrary-length list of blocks, and no support for adding, removing, or reordering DOM nodes and then re-serializing the result. Building this is **new UI/JS architecture, not an extension of an existing pattern**. Scope and estimate this story accordingly: it is materially larger and riskier than a typical "add a field to the editor" task, and the team should treat it as warranting its own design/spike pass before full implementation. Confirm with the user whether drag-and-drop reordering is required for v1, or whether simpler up/down arrow buttons are acceptable to ship faster (the spec leaves this open).

This story also introduces a second, functionally separate new control: a page-level multi-select tag picker (`TagPicker.ts`), and a per-block mobile-image-position toggle (before/after) for text-image blocks, independent of the desktop left/right variant (Decision #6).

This story depends on: the Architectural rule amendment story (this is the concrete implementation of the exception that story documents — it must not begin until that amendment is merged), the Twig templates + SCSS story (the editor clones/binds real server-rendered markup, so the block partials must exist first), and the Tag entity + admin CRUD story (`TagPicker.ts` calls `GET /api/admin/tags`, which must exist first).

## 2. Le besoin (User Story)

```
En tant qu'administrateur éditant un article Actualité en mode édition inline
Je souhaite ajouter, supprimer et réordonner des blocs de contenu (texte ou texte+image), régler la position mobile de l'image par bloc, et assigner des tags à l'article
Afin de composer librement le contenu d'un article sans dépendre d'une structure fixe définie à la création de la page
```

## 3. Liens utiles (Useful Links)

- **Spec source**: `spec-driven-development/2026-07-07-actualite-news-page-template.md`, task group "Inline Editor — Dynamic/repeatable blocks UI (new capability — no precedent...)"
- **Files to extend/create**:
  - `assets/admin/editor/ContentSerializer.ts` (extend `serializeSectioned()` or add new methods)
  - `assets/admin/editor/BlockListEditor.ts` (new)
  - `assets/admin/editor/TagPicker.ts` (new)
  - `assets/admin/editor/ChangeTracker.ts` (extend dirty-state tracking)
  - `assets/admin/editor/EditorCore.ts` (check for/add exported `bindEditableElement()` for late-bound MediumEditor initialization)
  - `assets/styles/admin/_editor.scss` (add/remove/reorder controls, mobile-position toggle, guarded behind `editMode`)
- **Architectural basis**: `spec-driven-development/rules/section-component-architecture.md`, amended "Repeatable Block Templates" exception section (see Story "Architectural rule amendment") — must be merged first
- `medium-editor` skill (toolbar/extension conventions for newly-bound elements)
- **Existing media-picker flow**: check for the modal/flow triggered by `data-editor-type="image"` elsewhere in the editor — reuse it, do not build a second one
- **Blocking dependencies**: Story "Architectural rule amendment" (`stories/actualite-architecture-rule-amendment.md`); Story "Twig templates + SCSS for Actualité page" (`stories/actualite-twig-scss.md`); Story "Tag entity + admin CRUD" (`stories/actualite-tag-entity-admin-crud.md`)

## 4. Critères d'acceptation (Acceptance Criteria)

### Scénario 1: Ajout d'un bloc
```gherkin
Scénario: L'administrateur ajoute un nouveau bloc de contenu
  Étant donné que je suis en mode édition sur un article Actualité
  Quand je clique sur le contrôle "Ajouter un bloc" et choisis un type parmi les 3 disponibles (texte, texte+image gauche, texte+image droite)
  Alors un nouveau bloc est instancié côté client (clonage d'un fragment Twig masqué déjà rendu par le serveur, pas de construction de DOM via des littéraux de template TS)
  Et ce bloc reçoit un identifiant stable généré côté client via `crypto.randomUUID()` (jamais de collision avec les blocs existants)
```

### Scénario 2: Suppression d'un bloc avec confirmation
```gherkin
Scénario: L'administrateur supprime un bloc existant
  Étant donné qu'un article contient plusieurs blocs
  Quand je clique sur "Supprimer" pour un bloc donné
  Alors une confirmation est demandée avant suppression effective (pour éviter une perte de contenu accidentelle)
  Et une fois confirmée, le bloc est retiré du DOM et ne fait plus partie de la sérialisation lors de la sauvegarde suivante
```

### Scénario 3: Réordonnancement des blocs
```gherkin
Scénario: L'administrateur déplace un bloc à une nouvelle position
  Étant donné 5 blocs de types mixtes affichés dans un ordre donné
  Quand je réordonne le bloc en position 4 vers la position 1 (via drag-and-drop ou boutons haut/bas, selon la décision v1 confirmée avec l'utilisateur)
  Alors l'ordre visuel du DOM reflète immédiatement le nouvel ordre
  Et les identifiants stables (UUID) des blocs ne changent pas lors du réordonnancement
```

### Scénario 4: Sérialisation et sauvegarde du scénario complet ajout/suppression/réordonnancement
```gherkin
Scénario: Le cycle complet ajout/suppression/réordonnancement produit le tableau blocks attendu côté serveur
  Étant donné que j'ajoute 5 blocs de types mixtes, supprime le bloc #2, puis déplace le bloc #4 en position #1
  Quand je sauvegarde
  Alors le tableau `blocks` envoyé au serveur reflète exactement cet état final (ordre, types, contenu)
  Et `ActualitePageContentSaver::validateAndMapContent()` accepte ce payload sans erreur de validation, y compris pour un bloc nouvellement ajouté et jamais sauvegardé auparavant
```

### Scénario 5: Toggle de position mobile de l'image par bloc
```gherkin
Scénario: La position mobile de l'image est réglable indépendamment par bloc
  Étant donné un bloc texte+image affiché dans l'éditeur, avec un contrôle "position de l'image sur mobile" (avant/après), visible uniquement pour ce type de bloc
  Quand je bascule ce contrôle sur "après" pour le bloc A, sans toucher au bloc B
  Alors la valeur `mobileImagePosition` du bloc A change, tandis que celle du bloc B reste inchangée
  Et la valeur par défaut à la création d'un nouveau bloc texte+image est "avant" (`before`)
  Et la sauvegarde persiste cette valeur distinctement par bloc
```

### Scénario 6: Liaison de MediumEditor sur les blocs ajoutés dynamiquement
```gherkin
Scénario: Les champs éditables d'un bloc nouvellement ajouté fonctionnent comme ceux rendus au chargement
  Étant donné un bloc ajouté dynamiquement après le chargement de la page
  Quand je clique dans son champ title/subtitle/content pour taper du texte
  Alors MediumEditor est actif sur ce champ exactement comme sur un bloc rendu par le serveur au chargement (même barre d'outils, même comportement)
  Et cette liaison passe par une fonction exportée (ex. `bindEditableElement()`) appelée après insertion DOM, pas seulement au bootstrap initial de l'éditeur
```

### Scénario 7: Insertion d'image dans un bloc texte+image
```gherkin
Scénario: L'administrateur insère une image via le flux existant du media-picker
  Étant donné un bloc texte+image nouvellement ajouté, sans image
  Quand je clique sur le champ image (`data-editor-type="image"`)
  Alors le même modal/flux de sélection de média que celui utilisé ailleurs dans l'éditeur s'ouvre (aucun second flux dupliqué n'est créé)
  Et l'image sélectionnée est correctement associée à ce bloc lors de la sauvegarde
```

### Scénario 8: Sélecteur de tags au niveau de la page
```gherkin
Scénario: L'administrateur gère les tags assignés à l'article via TagPicker.ts
  Étant donné que `TagPicker.ts` charge la liste des tags via `GET /api/admin/tags`
  Quand je sélectionne ou désélectionne un tag existant
  Alors le payload de sauvegarde en attente est mis à jour immédiatement (chip visible dans l'UI)
  Et la sauvegarde persiste exactement l'ensemble de tags sélectionné, via le chemin de sauvegarde confirmé (probablement distinct du endpoint de sauvegarde de `content`, puisque les tags sont une relation d'entité)
  Et la création de tag à la volée depuis le picker n'est incluse que si explicitement confirmée en scope v1 avec l'utilisateur (sinon, l'admin doit pré-créer les tags via l'écran d'administration dédié)
```

### Scénario 9: Suivi de l'état "modifié" (dirty state)
```gherkin
Scénario: ChangeTracker détecte les modifications liées aux blocs et aux tags
  Étant donné que j'ajoute, supprime ou réordonne un bloc, modifie une position mobile d'image, ou change la sélection de tags
  Quand ces actions se produisent, sans qu'aucun champ de texte ne soit modifié
  Alors `ChangeTracker.ts` détecte un état "modifié" au même titre qu'une modification de champ classique
  Et l'indicateur de sauvegarde en attente (ou équivalent) reflète cet état
```

### Scénario 10: Isolation des contrôles d'édition
```gherkin
Scénario: Les contrôles d'ajout/suppression/réordonnancement n'apparaissent jamais en dehors du mode édition
  Étant donné qu'un visiteur non authentifié consulte la page publique (sans `?edit=1`)
  Quand la page est rendue
  Alors aucun contrôle d'administration (bouton ajouter/supprimer, drag handle, toggle mobile-position) n'est visible ni présent dans le DOM public
  Et ces styles sont définis dans `assets/styles/admin/_editor.scss`, jamais mélangés au CSS public
```
