# Éditeur de Landing Pages (GrapesJS) — Guide de reprise

Ce document existe pour qu'une personne qui n'a jamais touché au projet puisse comprendre **comment l'outil fonctionne**, **où se trouve quoi**, **ce qui a été refait pendant la refonte de septembre 2026** et **quels pièges l'attendent**.

*Dernière mise à jour : 11 septembre 2026 — branche `feature/duplicationLP` (refonte UI mergée depuis `feature/editor-ui-redesign`).*

**À lire dans cet ordre si tu débarques :** section 1 (à quoi ça sert) → section 2 (vocabulaire GrapesJS) → section 3 (cycle de vie d'une page) → section 5 (l'écran d'édition) → section 10 (les pièges). Le reste se lit à la demande.

**Documents complémentaires, dans `plans/` :**

| Document | Contenu |
| :--- | :--- |
| `DOCUMENTATION_GRAPESJS.md` | La doc historique (janvier 2026) : liste des blocs, design system, métadonnées, assets. Toujours valable, mais antérieure à la refonte. |
| `DOCUMENTATION_PAGES_FILLES.md` | Le concept parent/enfant côté métier. |
| `DOCUMENTATION_SETTINGS_PANEL.md` | Le panneau Paramètres (traits) en détail. |
| `DOCUMENTATION_SIDEBAR_RIGHT.md.resolved` | La sidebar droite (Style + Traits). |
| `DOCUMENTATION_STYLE_MANAGER.md` | Les secteurs du Style Manager en détail. |
| `DOCUMENTATION_SIDEBAR_STYLE.md` | Le format « quad » compact des marges dans le secteur Dimension. |
| `DOCUMENTATION_DUPLICATION_MULTIPLE.md` + `PLANIFICATION_EXCEL_DUPLICATION.md` | Le moteur de duplication en masse via Excel. |
| `SUIVI_CORRECTIONS_HEADER.md` | Journal des corrections du header (7 septembre 2026). |

---

## 1. À quoi sert l'outil

Une **landing page (LP)** est une page marketing autonome, construite sans code par l'équipe métier : jeux à gratter, formulaires de participation, coupons, localisation du magasin le plus proche, etc.

L'outil se compose de trois choses distinctes, et c'est la première confusion à éviter :

1. **L'éditeur** — un écran d'administration où l'on construit la page en glissant des blocs (`/landing_websites/{id}/editGjs`).
2. **La page publiée** — le rendu réellement servi au visiteur (`/landing_websites/{id}` côté admin, et l'URL courte publique `https://c-lic.fr/{slug}`). Ce n'est pas la même page que le canvas de l'éditeur : elle exécute ses propres scripts.
3. **Le moteur de duplication** — fabrique N landing pages à partir d'une LP modèle et d'un fichier Excel (une LP par magasin, par ville, par opération…).

Un principe structurant : dans l'éditeur, beaucoup de blocs affichent un **contenu d'exemple statique** ; sur la page publiée, ce contenu est **remplacé à l'exécution** par les vraies données (magasin le plus proche, valeurs de variables, etc.). Tant qu'on n'a pas ça en tête, le comportement des blocs métier paraît incohérent.

---

## 2. Le vocabulaire GrapesJS (les 8 notions indispensables)

GrapesJS (version **0.22.13**, dans `node_modules/grapesjs`) est la bibliothèque d'édition visuelle. Ses notions se recoupent et portent des noms proches : voici la grille de lecture.

| Notion | Ce que c'est | Où ça vit dans le projet |
| :--- | :--- | :--- |
| **Block** (bloc) | La **vignette** qu'on glisse depuis le panneau « + ». Ne contient qu'une étiquette, une icône, une catégorie et le HTML initial à déposer. | `resources/js/grapejs/blocks/*.js` |
| **Component** (composant) | Le **type** d'élément une fois posé dans la page : son modèle, son nom affiché, son icône dans les calques, ses règles (déplaçable, éditable…) et **sa liste de traits**. | `resources/js/grapejs/components/*.js` |
| **Trait** | Un **champ de réglage** affiché dans l'onglet « Options » à droite quand le composant est sélectionné. Écrit en général un attribut `data-*` sur l'élément. | `resources/js/grapejs/traits/*.js` |
| **Style Manager** | Le panneau **CSS** (onglet pinceau) : dimensions, typographie, décorations. Produit du vrai CSS, stocké avec la page. | `resources/js/grapejs/style.js` |
| **Layer Manager** | L'arborescence **Calques** dans la sidebar gauche. | `resources/js/views/landing_website_layers_panel.js` |
| **Canvas** | La zone centrale. **C'est une iframe** : son document est séparé de celui de l'admin. Conséquence quotidienne : un `document.querySelector` depuis l'admin ne voit pas le contenu de la page, et inversement. | configuré dans `landing_website_gjs.js` (`canvas.styles`) |
| **protectedCss** | Un CSS « système », non éditable par l'utilisateur, injecté dans le canvas **et exporté avec le CSS de la page**. C'est là que vivent les règles indispensables au fonctionnement des composants (positionnement des jeux, styles du Drive…). | tableau `protectedCss` dans `landing_website_gjs.js` |
| **Storage Manager / Undo Manager** | La sauvegarde (vers le serveur) et l'historique annuler/rétablir. Attention, leurs comportements sont contre-intuitifs : voir sections 7 et 10. | `landing_website_gjs.js`, `landing_website_toolbar.js` |

**Règle de décision quand tu dois modifier quelque chose :**

- la vignette dans le panneau « + » → `blocks/`
- le HTML déposé dans la page → `blocks/` (propriété `content`)
- le nom, l'icône, les règles, la liste des réglages → `components/`
- un champ de réglage → `traits/`
- l'apparence de l'éditeur lui-même → `resources/sass/landing.scss`
- l'apparence du bloc **sur la page publiée** → `protectedCss` ou un `.scss` importé dedans
- le comportement du bloc **chez le visiteur** → `resources/js/views/landing_website/*.js`

---

## 3. Cycle de vie d'une page

```mermaid
graph TD
    A["Création<br/>store() — vierge, depuis un template,<br/>ou page fille d'une catégorie"] --> B["Édition<br/>editGjs() -> edit_gjs.blade.php"]
    B --> C["Sauvegarde automatique<br/>tous les 10 changements<br/>POST /template/store"]
    B --> D["Sauvegarde manuelle<br/>bouton Enregistrer<br/>(template + nom + tags)"]
    C --> E["Colonne gjs<br/>(html, css, pages, styles)"]
    D --> E
    E --> F["Page publiée<br/>show.blade.php<br/>+ scripts landing_website_show.js"]
    B --> G["Aperçu<br/>ouvre la page publiée<br/>dans un onglet"]
    E --> H["Duplication en masse<br/>Excel -> N LPs"]
```

Point important, et vérifié : **il n'existe pas de notion de brouillon**. La sauvegarde automatique écrit dans la même colonne `gjs` que celle lue par la page publiée (`loadTemplate()` renvoie `$landingWebsite->gjs`, et `show.blade.php` affiche `$landingWebsite->gjs['css']`). Autrement dit, toutes les 10 modifications, un travail en cours part en ligne. Voir section 10.

---

## 4. Carte du code

### Front — l'éditeur

| Chemin | Rôle |
| :--- | :--- |
| `resources/js/views/landing_website_gjs.js` | **Point d'entrée.** `grapesjs.init({...})` : où s'accrochent les panneaux, le stockage, les devices, le `protectedCss`, la locale FR. Câble aussi les boutons de la toolbar et les modules ci-dessous. |
| `resources/js/views/landing_website_toolbar.js` | État du bouton Enregistrer (propre / non enregistré / en cours / échec), activation des boutons Annuler-Rétablir, toasts. |
| `resources/js/views/landing_website_blocks_panel.js` | Position et ouverture/fermeture du panneau flottant « Blocs / Effets » (tokens CSS, `ResizeObserver`, Échap, clic extérieur). |
| `resources/js/views/landing_website_layers_panel.js` | Section Calques : nom de la racine, déplier/replier, ouverture automatique des parents du calque sélectionné. |
| `resources/js/views/landing_website_tags_sync.js` | « Smart Tagging » : ajoute automatiquement des tags selon les composants présents dans la page. |
| `resources/js/views/landing_website_sidebar_uploads.js` | FilePond dans la modale « Éléments de la page » (favicon, image OG) + compteurs de caractères. |
| `resources/js/views/landing_website_og_metadata.js` | Sauvegarde AJAX des métadonnées de la sidebar (`.js-save-sidebar-metadata`). |
| `resources/js/views/landing_website_device_switcher.js` | Choix et mémorisation de l'appareil (iPhone 16, Galaxy…). |
| `resources/js/views/landing_website_sidebar_resizer.js` | Redimensionnement des sidebars. |
| `resources/js/utils/toast.js` | Notifications éphémères (`showToast`). |
| `resources/js/utils/sidebarToggle.js` | Repli de la sidebar gauche. |
| `resources/js/components/variable-schema/` | Sous-arbre **React** (21 fichiers) de la section « Variable » : schémas de variables, jeux de données, aperçu fusionné. |
| `resources/js/grapejs/` | Le cœur GrapesJS : `blocks/` (28 fichiers), `components/` (28), `traits/` (10 fichiers), `utils/`, `plugins/code-editor.js`, `style.js`, `commands.js`. |
| `resources/sass/landing.scss` | Styles de l'éditeur (grille, toolbar, sidebars, calques, toasts). **Fichier avec BOM UTF-8.** |
| `resources/sass/_drive.scss` | Styles du bloc Drive, injectés dans `protectedCss` via un import Vite `?inline`. |
| `resources/sass/_drive_traits.scss` | Mise en page en grille du panneau de réglages du Drive. |

### Front — la page publiée

| Chemin | Rôle |
| :--- | :--- |
| `resources/js/views/landing_website_show.js` | Chef d'orchestre des scripts exécutés chez le visiteur. |
| `resources/js/views/landing_website/drive.js` | Géolocalisation, chargement du CSV magasins, rendu final du bloc Drive. |
| `resources/js/helpers/helpers.js` | Utilitaires partagés (`getLocation`, `fetchJSONFile`, `isDarkBgColor`…). |

### Back — Laravel

| Chemin | Rôle |
| :--- | :--- |
| `app/Http/Controllers/LandingWebsiteController.php` | Tout : `editGjs()` (affiche l'éditeur), `store()` (création), `storeTemplate()` / `loadTemplate()` (sauvegarde du gjs), `updateSidebarMetadata()` (métadonnées AJAX), `duplicateMultiple()` et la famille duplication. |
| `app/Models/LandingWebsite.php` | Colonne `gjs` (JSON), `parent_id`, `is_template`, relations `parent()` / `children()`, tags Spatie. |
| `app/Services/GjsVariableReplacer.php` | Remplacement des `${variable}` dans l'arbre GJS lors des duplications. |
| `resources/views/pages/landing_website/edit_gjs.blade.php` | Conteneur de l'éditeur. |
| `resources/views/pages/landing_website/editor/_toolbar.blade.php` | Barre du haut. |
| `resources/views/pages/landing_website/editor/_sidebar_left.blade.php` | Sidebar gauche (5 sections + panneau flottant des blocs). |
| `resources/views/pages/landing_website/editor/_sidebar_right.blade.php` | Sidebar droite (44 lignes : en-tête dynamique, 2 onglets, 3 conteneurs). |
| `resources/views/pages/landing_website/show.blade.php` | La page publiée. Injecte `{!! $landingWebsite->gjs['css'] !!}`. |

### Les routes qui comptent

| Méthode | URI | Contrôleur | Quand |
| :--- | :--- | :--- | :--- |
| GET | `landing_websites/{lw}/editGjs` | `editGjs` | Ouvre l'éditeur |
| POST | `landing_websites/{id}/template/store` | `storeTemplate` | **Chaque sauvegarde** (auto ou manuelle) |
| GET | `landing_websites/{id}/template/load` | `loadTemplate` | Chargement du contenu au démarrage |
| PUT | `landing_websites/{lw}/update_sidebar_metadata` | `updateSidebarMetadata` | Nom, slug, titre SEO, favicon, OG, tags, actif/template |
| POST | `landing_websites/{id}/asset/store` | `storeAsset` | Upload d'images dans l'éditeur |
| POST | `landing_websites/stores/csv` + `/store_csv` | `getStoresCsv` | Lecture du CSV magasins (Drive / Store Locator) |
| GET | `landing_websites/{lw}/duplicate-page` | `duplicatePage` | Page de duplication en masse |
| POST | `landing_websites/{lw}/duplicate-multiple` | `duplicateMultiple` | Génère N LPs |

---

## 5. L'écran d'édition, zone par zone

### 5.1 Le conteneur

`edit_gjs.blade.php` fait quatre choses à connaître :

1. Il dessine la grille `.editor-row` : toolbar en haut, puis sidebar gauche / canvas / sidebar droite.
2. Il enveloppe le canvas dans une **maquette de téléphone** (`.smartphone-frame`, barre de statut, fausse barre d'adresse avec le slug). C'est purement décoratif, mais ça explique pourquoi le canvas est étroit.
3. Il injecte les données pour le JS : `const landingWebsite`, `landingWebsiteAssets`, `window.redirectPages`, `window.wpAdmin.allTags`, plus les attributs `data-landing-website` sur `#gjs`.
4. Il charge les scripts via `@vite` : `tags.js`, `landing_website_gjs.js`, `blocks_panel`, `layers_panel`, `sidebar_uploads`, `og_metadata`, `device_switcher`. Les autres modules (toolbar, tags_sync, toast…) sont importés par `landing_website_gjs.js`.

### 5.2 La toolbar

De gauche à droite : repli de la sidebar (`#btn-toggle-sidebar`), retour à la liste, **nom de la page** (`#toolbar-page-name`, sauvegardé avec les métadonnées), **Annuler / Rétablir** (`#btn-undo` / `#btn-redo`), **Variables** (`#btn-variables`), **éditeur de code** (`#btn-code-editor`, plugin `plugins/code-editor.js`), **Aperçu** (`#btn-preview`), **choix d'appareil** (`#deviceSwitcherBtn` : iPhone SE 320px, iPhone 15/16 393px, iPhone 15 Pro Max 430px, S23 Ultra 412px, iPad 768px, Desktop 720px), **vider la page** (`#btn-clear`), et à droite l'**état de sauvegarde** (`#editor-save-status`) avec le bouton **Enregistrer** (`#btn-save`).

### 5.3 La sidebar gauche

Cinq sections, dans cet ordre, plus le panneau flottant des blocs.

**Panneau flottant « Blocs / Effets »** — ouvert par le bouton rond « + ». Sa position est calculée en JS à partir de tokens CSS (`--wp-blocks-panel-*`) pour rester collé à la sidebar quelle que soit sa largeur. L'onglet « Effets » est encore un placeholder (« disponible prochainement ») ; attention, il ne faut pas le confondre avec le bloc « Effet » qui existe, lui, dans la catégorie Média.

Les blocs sont enregistrés dans `grapejs/blocks.js` (28 fichiers, dont `column.js` qui en déclare trois à lui seul : 1, 2 et 3 colonnes) et rangés en 4 catégories définies dans chaque fichier : `Structure`, `Base`, `Média`, `Forms`. L'ordre d'appel dans `blocks.js` détermine l'ordre d'affichage.

**Tags** — affichage **en lecture seule** de badges `#nom`. C'est un choix assumé : il n'existe plus d'interface d'ajout manuel. Les tags sont posés automatiquement par le « Smart Tagging » (`landing_website_tags_sync.js`), qui scanne les types de composants présents et en déduit une catégorie :

| Composant présent | Tag ajouté |
| :--- | :--- |
| `video` | Vidéo |
| `scratch`, `shaker`, `flipcard` | Jeu |
| `map`, `storeLocator`, `drive` | Localisation |
| `barcode` | Coupon |
| `form` | Formulaire |
| `gallery`, `slider` | Galerie |
| `effet` | Animation |

La couleur des badges vient d'une palette CSS fixe indexée sur le nom du tag (`landing.scss`, sélecteurs `[data-tag="jeu"]`, `[data-tag="localisation"]`…). Un tag hors de cette liste s'affiche avec le style neutre.

L'état des tags vit dans une instance **Tagify** attachée à un input caché (`#js-tagify-editor`, dans `_modal_tags.blade.php`), lue par le Smart Tagging **et** par les deux chemins de sauvegarde. Ne pas supprimer cet input : c'est le modèle de données des tags.

**Elements** — liste de pastilles (Slug, Favicon, Titre Preview, Description Preview, Preview) avec un point vert/gris selon que l'information est renseignée. Tout ouvre la même modale « Éléments de la page » (`_modal_elements.blade.php`), qui poste en AJAX vers `updateSidebarMetadata`. Limites côté serveur : nom 191, titre SEO 250, titre OG **16**, description OG **40**, URLs 255.

**Variable** — point de montage `#variable-schema-manager` d'un sous-arbre React (`resources/js/components/variable-schema/`, ~3 800 lignes) : définition des variables `${...}` d'une LP, jeux de données globaux / par destinataire, aperçu fusionné, découverte depuis un fichier. C'est la partie la moins auditée de la refonte ; elle mérite sa propre passe.

**Pages Filles** — la hiérarchie parent/enfant (`parent_id`). Le « + » ouvre une petite modale de création par **catégorie** (voir section 6.3).

**Calques** — le Layer Manager de GrapesJS, habillé maison : traits d'arbre, icône par composant, sélection orange, actions (déplacer, visibilité) révélées au survol avec une cible de 28 px et un accès clavier. Le nom de la racine est forcé au nom de la landing page.

### 5.4 Le canvas

Une iframe, à laquelle on injecte Bootstrap 5.3.2 et Font Awesome 6.5.1 (`canvas.styles`) ainsi que Bootstrap JS. Le `protectedCss` (27 règles + le contenu de `_drive.scss`) est injecté dans le canvas **et** exporté avec le CSS de la page.

*Calibrage des dimensions (septembre 2026)* : GrapesJS appliquait nativement un décalage interne (`right: ~61px`) sur `.gjs-cv-canvas` qui amputait la largeur affichée à l'intérieur du smartphone. Des règles CSS forcées (`width: 100% !important; left: 0; right: 0;`) garantissent désormais que l'iframe et son contenu occupent exactement 100% de la largeur de l'écran choisi.

### 5.5 La sidebar droite

Un en-tête dynamique (icône + nom du composant sélectionné, « Général » quand rien n'est sélectionné), deux onglets — pinceau (**STYLE**) et engrenage (**PARAMÈTRES**) — et trois conteneurs que GrapesJS remplit : `#selector-container` (classes CSS), `#styles-container` (Style Manager), `#traits-container` (Trait Manager).

Secteurs du Style Manager, définis dans `grapejs/style.js` : **Général, Flex, Dimension, Typographie, Décorations, Extra**. Le secteur Dimension affiche les marges au format « quad » compact (4 cellules sur une ligne) — voir `DOCUMENTATION_SIDEBAR_STYLE.md`.

Le filtrage des réglages par type de composant est **natif** : chaque composant déclare son tableau `traits`, et le panneau n'affiche que ceux-là. Il n'y a rien à coder pour ça.

---

## 6. Les composants métier

### 6.1 Le trio bloc / composant / traits

Ajouter un composant, c'est toucher trois endroits puis les enregistrer :

1. `grapejs/blocks/mon-bloc.js` — la vignette et le HTML initial :

```javascript
export default (editor) => {
    editor.BlockManager.add('mon-bloc', {
        label: `<svg class="gjs-block-svg">…</svg><div class="gjs-block-label">Mon bloc</div>`,
        category: 'Média',
        content: `<div data-name="mon-bloc">Contenu d'exemple</div>`,
    });
};
```

2. `grapejs/components/mon-bloc.js` — le type, reconnu par son attribut `data-name`, avec son icône (elle apparaît dans les Calques), son nom lisible et ses réglages :

```javascript
comps.addType('mon-bloc', {
    extend: 'text',
    isComponent: el => el.getAttribute?.('data-name') === 'mon-bloc' ? { type: 'mon-bloc' } : false,
    model: {
        defaults: {
            'custom-name': 'Mon bloc',
            icon: '<i class="fa fa-star"></i>',
            traits: [ /* voir 6.2 */ ],
        },
    },
});
```

3. Enregistrer le bloc dans `grapejs/blocks.js` et le composant dans `grapejs/components.js` (l'ordre d'appel détermine l'ordre d'affichage dans le panneau).

4. `npm run build`, puis recharger l'éditeur.

### 6.2 Les traits

Les traits simples sont des objets partagés dans `traits/traits.js` (`{ name: 'data-mon_champ', label: 'Mon champ', type: 'text' | 'number' | 'color' | 'checkbox' | 'select' }`).

Quand un champ demande une vraie interface (galerie, upload, sélection de page de redirection, séparateur de section), on crée un **type de trait custom** — le pattern est déjà utilisé six fois dans le projet :

```javascript
editor.TraitManager.addType('group-header', {
    createInput({ trait }) {
        const heading = document.createElement('div');
        heading.className = 'gjs-trait-group-header';
        heading.textContent = trait.get('label');
        return heading;
    },
});
```

Types custom existants, vérifiés dans `traits/` : `uploadImg`, `gallery`, `effect`, `csv` (store locator), `redirect-pages`, `select-options`, `textarea`, `variable_select`, `variable_attr_select`, `group-header`.

⚠️ **Piège vérifié** : les `attributes` d'un trait sont posés sur le **champ de saisie**, pas sur la ligne du panneau. Un `attributes: { 'data-full-width': 'true' }` n'est donc pas exploitable en CSS sur le conteneur de la ligne sans passer par `:has()`.

### 6.3 Pages filles par catégorie

Le « + » de la section Pages Filles propose six catégories, définies côté serveur dans la constante `LandingWebsiteController::CHILD_PAGE_TEMPLATE_NAMES` : **Remerciements, Jeu, Règlement, Coupon, Vidéo, Localisation**.

Le mécanisme, vérifié de bout en bout :

1. `editGjs()` calcule `$childPageTemplateOptions` : pour chaque catégorie, il cherche une LP avec `is_template = true`, le **nom exactement égal** à la catégorie, et sans enfant. S'il en trouve exactement une, il expose son `templateId` ; sinon `null`.
2. La modale (`_modal_create_child.blade.php`) construit un `<select name="template_id">` : les catégories sans modèle unique sont **désactivées** et affichent « (modèle indisponible) », ce qui évite de créer silencieusement une page vide.
3. À la création, `store()` part dans la branche `template_id` : il duplique le modèle (`replicate()`), force le nom sur celui du modèle grâce au drapeau `use_category_template`, et rattache la nouvelle page au parent.

**Pour ajouter un modèle de catégorie** (côté métier, sans code) : construire la page dans l'éditeur, la renommer **exactement** comme la catégorie, cocher « Template » dans la modale Éléments. Elle devient immédiatement proposée dans le « + ».

### 6.4 Cas d'école : le bloc « Drive to store »

C'est le composant le plus complet ; il illustre tous les mécanismes du projet.

**Ce qu'il fait chez le visiteur** : affiche le magasin le plus proche (géolocalisation) ou un magasin désigné par une variable, avec son adresse, ses horaires, un lien site, un bouton Appeler et un bouton S'y rendre.

**Les pièces :**

| Fichier | Rôle |
| :--- | :--- |
| `grapejs/blocks/drive.js` | Vignette + contenu d'exemple. Le contenu est structuré avec les **mêmes classes** que le rendu publié (`drive-name`, `drive-address`, `drive-hours`, `drive-map-title`, `link-website`, `btn-call`, `btn-go`). |
| `grapejs/components/drive.js` | Type `drive` : ~20 traits regroupés en sections, `groupedTraits: true`, synchro de `variable_store` vers `data-variable_store`. |
| `grapejs/utils/drivePresentation.js` | `getDriveStyles(attributes)` → l'objet de variables CSS (`--drive-name-size`, `--drive-map-title-color`…), **partagé entre l'éditeur et la page publiée** : c'est ce qui garantit que les deux affichent la même chose. |
| `sass/_drive.scss` | Les règles qui **consomment** ces variables. Injecté dans `protectedCss`, donc présent dans le canvas et exporté avec la page. |
| `sass/_drive_traits.scss` | La mise en page 2 colonnes du panneau de réglages. |
| `views/landing_website/drive.js` | Le comportement chez le visiteur : géolocalisation, appel `/store_csv`, `displayStore()` qui réécrit le contenu. |

**La chaîne complète d'un réglage** (à comprendre une fois pour toutes) : un trait écrit `data-map_label_font_size="44"` sur l'élément → `getDriveStyles()` transforme l'attribut en variable CSS `--drive-map-title-size: 44px` posée sur l'élément → `_drive.scss` applique `font-size: var(--drive-map-title-size, 16px)` sur `.drive-map-title`. **Si un seul maillon manque, le réglage ne produit rien** — c'est exactement le bug trouvé pendant la refonte (les variables étaient posées, aucune règle ne les lisait).

**Les données magasins** viennent d'un CSV attaché à la LP (trait « Lieux ») avec les colonnes `name`, `address`, `phone`, `opening_time`, `link`, `latitude`, `longitude`.

**Deux modes d'affichage :**
- *variable* (`data-variable_store` renseigné) : pas de géolocalisation, la LP affiche directement le magasin de l'index donné — c'est ce mode que la duplication en masse utilise pour générer une LP par magasin ;
- *géolocalisation* : le visiteur clique sur « Afficher le lieu le plus proche », le navigateur demande sa position, le plus proche est calculé.

Rappel : la géolocalisation exige un **contexte sécurisé** (HTTPS ou localhost). En HTTP simple, ce mode ne peut pas aboutir.

---

## 7. Sauvegarde, aperçu, publication

### Ce qui est configuré

```javascript
storageManager: {
  type: 'remote',
  stepsBeforeSave: 10,   // sauvegarde auto tous les 10 changements
  autoload: true,
  onStore: (data, editor) => ({ ...data, html: editor.getHtml(), css: editor.getCss(), name: /* nom de la toolbar */ }),
  options: { remote: { urlStore: '…/template/store', urlLoad: '…/template/load' } },
}
```

Les valeurs par défaut de GrapesJS étant `autosave: true` et `stepsBeforeSave: 1`, ce réglage signifie : **la sauvegarde automatique est active et se déclenche tous les 10 changements internes. Il n'y a aucun déclencheur temporel.** Si tu fais 9 modifications et que tu fermes l'onglet, rien n'est parti.

### Les deux chemins de sauvegarde

- **Automatique** : GrapesJS poste le projet vers `/template/store`.
- **Manuelle** (bouton Enregistrer, commande `save` dans `commands.js`) : `Promise.all([e.store(), saveMetadata()])` — le template **et** le nom + les tags, via `update_sidebar_metadata`.

Le bouton reflète l'état : *Aucune modification* / *Modifications non enregistrées* (orange) / *Enregistrement…* / *Enregistré à HH:MM* / *Échec* avec bouton Réessayer. Depuis septembre, des toasts s'affichent aussi : « Action annulée », « Action rétablie », « Sauvegarde automatique à HH:MM », « Échec de la sauvegarde automatique ».

### L'aperçu

Le bouton Aperçu sauvegarde, puis ouvre `/landing_websites/{id}?preview=1&device={device}` dans un nouvel onglet : **c'est la vraie page publiée**, pas une simulation. Deux mécanismes clés s'y activent :
- Le bloc Drive conserve son magasin d'exemple au lieu de vider sa zone de contenu (pas besoin de géolocalisation pour vérifier le rendu).
- La page s'ouvre **immédiatement à la largeur exacte** du device actif dans l'éditeur (ex: 393px pour iPhone 16).
- Une barre d'outils discrète ([preview_bar.blade.php](resources/views/pages/landing_website/partials/preview_bar.blade.php)) permet d'alterner en un clic entre **Mobile** (largeur venant de l'éditeur), **Tablette (768px)** et **Desktop (720px)**, avec un bouton pour masquer la barre et contempler la page en plein écran.
- L'administrateur peut prévisualiser la landing page même si celle-ci n'est pas encore activée publiquement.

### Le CSS de la page publiée

`show.blade.php` injecte `{!! $landingWebsite->gjs['css'] !!}`. Ce CSS contient le style créé par l'utilisateur **et** le `protectedCss`, mais il n'est régénéré **qu'au moment d'une sauvegarde**. Conséquence à retenir : quand on ajoute une règle au `protectedCss`, les LP déjà publiées ne l'obtiennent qu'après avoir été rouvertes et sauvegardées.

---

## 8. Variables et duplication en masse

Résumé, le détail est dans `DOCUMENTATION_DUPLICATION_MULTIPLE.md` et `PLANIFICATION_EXCEL_DUPLICATION.md` :

- on place des `${ma_variable}` dans les textes et attributs d'une LP modèle ;
- la section « Variable » de la sidebar sert à déclarer ces variables et leurs jeux de valeurs ;
- la page « Dupliquer » propose : télécharger un modèle Excel → remplir hors ligne → uploader → prévisualiser → générer N LPs ;
- `App\Services\GjsVariableReplacer` parcourt récursivement l'arbre GJS et remplace les variables dans le contenu **et tous les attributs string** (`href`, `src`, `data-*`, `variable_store`…), profondeur max 50, limite 500 lignes ;
- 18 tests couvrent le moteur (accents, emojis, caractères HTML, valeurs vides, injection `variable_store`).
- La **v2 « component-aware »** (exposer dans l'Excel tous les composants éditables détectés, pas seulement les variables texte) est planifiée, pas implémentée.

---

## 9. Ce qui a été fait pendant la refonte (septembre 2026)

Ordre chronologique des commits sur `feature/editor-ui-redesign` / `feature/duplicationLP` :

| Commit | Contenu |
| :--- | :--- |
| `feat(landing-editor): Add synchronized device preview bar` | Barre d'aperçu synchronisée (Mobile / Tablette / Desktop) et prévisualisation des pages inactives. |
| `feat(landing-editor): Calibrate canvas dimensions and add contrast device presets` | Dimensions du canvas, suppression de l'offset GrapesJS et presets d'appareils (iPhone 15/16, S23 Ultra, Tablette, Desktop). |
| `feat(landing-editor): Add presence tracking and editor notifications` | Ajout initial de la présence et des toasts. La présence a ensuite été retirée ; les notifications de sauvegarde et d'historique sont conservées. |
| `feat(grapesjs): Add contextual style manager` | Secteurs du Style Manager contextualisés. |
| `fix(landing-editor): Align Drive settings style and fix color picker overlays` | Styles du panneau Drive, superposition des color pickers. |
| `feat(landing-editor): Add save feedback and history controls` | États du bouton Enregistrer, activation Annuler/Rétablir. |
| `fix(landing-editor): Apply Drive styles and handle loading errors` | `_drive.scss` + `drivePresentation.js` : les réglages de taille et de couleur produisent enfin un effet. |
| `fix(landing-editor): Center floating blocks toggle` | Centrage vertical du bouton « + » du panneau de blocs. |
| `ref(landing-editor): Organize Drive settings` | Réglages du Drive regroupés en sections (type de trait `group-header`). |
| `fix(landing-editor): Stabilize duplication editor` | Gros lot : nettoyage des calques, modale Éléments, pages filles par catégorie, tags. |
| *(non commité au moment d'écrire)* | Impasses du parcours Drive côté visiteur, toasts. |

### Détail par section auditée

**Calques.** Suppression d'un mécanisme de surlignage d'ancêtres mort (il s'appuyait sur un `data-cid` que GrapesJS ne pose pas) au profit du tracé d'arbre existant ; nom de la racine aligné sur le nom de la LP ; icône manquante sur le composant Shaker ; correction d'une couleur de sélection qui pointait sur une variable CSS inexistante ; cibles de 28 px et accès clavier sur les actions de ligne.

**Pages filles.** Neutralisation d'une modale orpheline (`_modal_children.blade.php` supprimée du disque ; son `@include` et la commande GrapesJS `children` avaient été retirés). Surtout : le sélecteur de catégorie était **purement décoratif** — les sept options créaient toutes la même page vierge. Il est maintenant branché sur de vrais modèles (section 6.3).

**Modale Éléments.** Aperçu OG cassé quand aucune image n'était définie (`src="#"`) ; upload favicon qui ne transmettait pas le fichier ; libellé de section imprécis ; garde-fou de longueur manquant sur le titre SEO ; suppression d'un listener AJAX mort (il visait une classe CSS qui n'existait sur aucun bouton).

**Tags.** Section confirmée en lecture seule ; la modale de gestion manuelle, devenue inaccessible, a été réduite au strict nécessaire (l'input Tagify caché) ; suppression d'un bloc qui réécrivait les badges juste avant un `location.reload()` ; retrait du champ `color` de la réponse JSON — la table `tags` **n'a pas** de colonne `color`, la valeur était toujours nulle.

**Panneau Blocs.** Module de positionnement réécrit autour de tokens CSS ; correction du centrage vertical du bouton flottant. **Reste à faire** : la catégorie `Forms` est le seul libellé resté en anglais alors que l'UI affiche Structure / Base / Média.

**Sidebar droite.** Nettoyage du code mort : suppression de `toggleSector` et de ses appels ciblant des identifiants inexistants (`traits-sector`, `selectors-sector`), suppression du doublon de calcul du nom de composant dans `toggleSectors` au profit de `updateHeaderStats`.

**Drive to store.** Chaîne de style rendue effective ; réglages regroupés ; tailles passées en champs numériques bornés (8–72) ; contenu d'exemple restructuré avec les classes du rendu réel ; côté visiteur, trois impasses silencieuses corrigées (aucun CSV configuré, géolocalisation refusée ou sans réponse, CSV en erreur) avec message explicite et bouton réaffiché pour réessayer, plus un garde-fou sur CSV vide qui provoquait une erreur JS.

**Toolbar / historique.** États du bouton Enregistrer, activation des boutons d'historique, toasts, et `?preview=1`.

**Retrait de la présence (15 septembre 2026).** Suppression du compteur et de la liste des utilisateurs, du module de polling, des styles dédiés et des routes POST/DELETE `editor-presence/{landingWebsite}` avec leur contrôleur, requête et modèle. Les anciens endpoints répondent en 404. Les tests vérifient ce retrait ainsi que le maintien de la sauvegarde, de l'historique et de leurs notifications. Aucun verrouillage ni mécanisme de collaboration n'est ajouté.

---

## 10. Pièges vérifiés et dette connue

Cette section est la plus utile au repreneur : chaque point a été constaté en exécutant le code, pas déduit.

### Annuler / Rétablir

- L'historique empile des **changements internes de modèle**, pas des actions utilisateur, et regroupe ceux qui se produisent rapprochés dans le temps. Mesures : un déplacement de bloc = **3 entrées** ; une modification de trait = 1 ; taper du texte = 1 entrée, enregistrée **au moment où on quitte l'élément**, pas pendant la frappe ; déplier tous les calques = 0 (`open` n'est pas tracké).
- Conséquence observée : après « taper dans un texte puis déplacer un bloc », le premier Annuler ne produit rien de visible et le second annule **le texte et le déplacement en même temps**.
- Tant qu'un texte est en cours d'édition dans le canvas, `core:undo` **ne fait rien** (GrapesJS laisse la main au Ctrl+Z natif du navigateur).
- « Rétablir » ne fait jamais l'inverse de la dernière action : il **ré-applique la dernière action annulée**.
- **Pistes discutées, non implémentées** : fermer l'éditeur de texte avant d'exécuter undo/redo ; sortir les mutations programmatiques de l'historique avec `UndoManager.skip()`.

### Sauvegarde

- Pas de brouillon : l'autosave publie (section 3). C'est la dette la plus visible par les clients finaux.
- Aucun filet en cas de perte : GrapesJS propose une option `recovery` (sauvegarde locale de secours quand l'envoi au serveur échoue, avec proposition de restauration au chargement suivant) — elle est à `false`. Aucun avertissement `beforeunload` non plus.
- Le `protectedCss` n'arrive dans une page publiée qu'à la sauvegarde suivante de cette page.

### Divers

- La géolocalisation nécessite HTTPS (ou localhost).
- Deux gabarits HTML décrivent le bloc Drive (le contenu d'exemple et le gabarit de `displayStore()`) : ils doivent être maintenus en phase.
- La section « Variable » (React, ~3 800 lignes) n'a pas encore été auditée.
- `convertGmGjs()`, appelé par `storeTemplate()`, ne se trouve pas dans `app/` — il vient d'une dépendance. À creuser avant de modifier le format de stockage.

---

## 11. Recettes pratiques

**Styliser un bloc pour la page publiée.** Ne pas écrire la règle dans `landing.scss` (c'est le CSS de l'éditeur, il n'est pas exporté). L'ajouter dans `protectedCss` — soit directement dans le tableau de `landing_website_gjs.js`, soit dans un `.scss` importé en `?inline` comme `_drive.scss`.

**Ajouter un réglage à un composant existant.** Définir l'objet trait dans `traits/traits.js`, l'ajouter au tableau `traits` du composant, puis penser à la chaîne complète : qui lit l'attribut au rendu, et quelle règle CSS l'applique.

**Grouper des réglages en sections** (comme le Drive). Ajouter `groupedTraits: true` aux `defaults` du composant, insérer des `{ type: 'group-header', label: 'MA SECTION', name: 'group_ma_section' }` dans le tableau `traits`, et prévoir la règle de grille. Attention : aujourd'hui `_drive_traits.scss` cible les noms de traits **en dur** — à généraliser avec `:has()` si on étend le pattern.

**Déboguer un composant sur la page publiée.** Ouvrir la page publiée (pas le canvas), inspecter l'élément `[data-name="…"]`, vérifier dans l'ordre : les attributs `data-*` présents, les variables CSS posées sur l'élément, puis l'existence de la règle CSS qui les consomme.

**Vérifier ce que voit vraiment le visiteur.** Le canvas ne fait pas foi : il n'exécute pas `landing_website_show.js`. Toujours confirmer via l'Aperçu.

---

## 12. Conventions et environnement

- **Fins de ligne** : tout le dépôt est en **CRLF**. `resources/sass/landing.scss` a en plus un **BOM UTF-8**. Un éditeur qui « normalise » silencieusement produit des diffs de plusieurs centaines de lignes.
- **Compilation obligatoire** après toute modification JS/SCSS : `npm run dev` (watch) ou `npm run build`.
- **PHP** : `./vendor/bin/pint` pour le formatage, `php artisan test --filter=LandingWebsite` pour les tests. À savoir : la suite complète est actuellement bloquée par un test existant qui mélange Pest et le lanceur PHPUnit.
- **Déploiement du retrait de présence** : reconstruire les assets et régénérer le cache des routes si le déploiement en utilise un. Recharger les onglets d'édition déjà ouverts pour arrêter leur ancien polling. Aucune nouvelle migration n'est nécessaire. La migration historique `2026_09_10_153413_create_landing_website_presences_table.php` reste inchangée ; la table `landing_website_presences`, si elle existe, n'est plus utilisée par l'application. Sa suppression physique éventuelle doit être planifiée séparément ; aucun rollback ni effacement de données n'a été exécuté pour ce retrait.
- **URL de travail** : `/landing_websites/{id}/editGjs`.
- **Assets** : `WELLPACK_ASSETS_PATH` (chemin disque) et `WELLPACK_ASSETS_URL` (URL publique) dans `.env`.

---

## 13. Glossaire

| Terme | Signification |
| :--- | :--- |
| **LP** | Landing page. |
| **Page fille / enfant** | LP rattachée à une autre par `parent_id`. |
| **Template** | LP marquée `is_template = true`, utilisée comme modèle de duplication ou de page fille. |
| **gjs** | La colonne JSON qui contient tout le contenu de la page (html, css, pages, styles). |
| **Trait** | Champ de réglage d'un composant (panneau de droite, onglet engrenage). |
| **protectedCss** | CSS système injecté dans le canvas et exporté avec la page. |
| **Smart Tagging** | Attribution automatique des tags selon les composants présents. |
| **Drive to store** | Bloc qui affiche le magasin le plus proche du visiteur. |
| **Store Locator** | Bloc de carte avec l'ensemble des points de vente. |

---

*Rédigé le 11 septembre 2026. Pour toute section marquée « reste à faire » ou « non implémenté », le détail du raisonnement est dans l'historique des échanges de la refonte.*
