# VitiCore

VitiCore fournit des composants Twig et Symfony UX reutilisables depuis les projets hotes. Ce README sert de point d'entree pour savoir quel composant appeler, ou trouver son template, et comment le surcharger sans casser le comportement Symfony UX.

Pour les regles completes d'utilisation depuis un projet hote, lire aussi `AGENTS.md`. Pour une modification du coeur, lire `AGENTS.development.md`.

## Emplacements utiles

Les classes des composants sont ici :

```text
src/Bridge/Symfony/Bundle/Twig/Components
```

Les templates de reference sont ici :

```text
src/Bridge/Symfony/Bundle/Resources/views/components
```

Dans un projet hote installe via Composer, les memes fichiers sont generalement visibles sous :

```text
vendor/cpcreation/viti-core/src/Bridge/Symfony/Bundle/Twig/Components
vendor/cpcreation/viti-core/src/Bridge/Symfony/Bundle/Resources/views/components
```

## Choisir le bon composant

Un `TwigComponent` sert a rendre un bloc reutilisable cote serveur, sans re-rendu AJAX apres le chargement initial. C'est le bon choix pour une image, une notification, une liste de variantes ou un bloc de tracking.

Un `LiveComponent` sert a rendre une interface qui change apres une action utilisateur, avec un re-rendu AJAX gere par Symfony UX LiveComponent. C'est le bon choix pour les formulaires, l'ajout au panier, le checkout, les modals reactives et les filtres.

Exemples :

```twig
<twig:collection:ProductCollection collection="homepage-cuvees" title="Nos cuvees" limit="6" />

<twig:shop:ProductCard :product="product" />

<twig:shop:InformationBlocks zone="top" />

<twig:cart:CartForm />

<twig:site:ContactForm />
```

Utiliser `:` devant une prop quand la valeur est une expression Twig ou un objet PHP :

```twig
<twig:shop:ProductCard :product="product" />
```

Sans `:`, la valeur est une chaine :

```twig
<twig:collection:ProductCollection collection="homepage-cuvees" />
```

## Catalogue rapide

Cet inventaire est synchronise avec les commandes Symfony suivantes :

```bash
php bin/console debug:twig-component --no-interaction --no-ansi
php bin/console debug:live-component --no-interaction --no-ansi
```

Il recense les 44 composants fournis par VitiCore : 20 `TwigComponent` et 24 `LiveComponent`. Le composant `site:Footer`, fourni par `cpcreation/viti-theme-bundle`, est egalement indique car VitiCore contient maintenant son template de repli. Les composants anonymes internes de Turbo et les autres composants propres aux themes ne sont pas reproduits ici.

| Composant Twig | Type | Template | Usage principal | Props publiques utiles |
| --- | --- | --- | --- | --- |
| `Image` | TwigComponent | `@components/Image.html.twig` | Image responsive via Glide ou chemin public | `path`, `pathLandscape`, `pathPortrait`, `ratio`, `ratioLandscape`, `ratioPortrait`, `alt`, `legend`, `mapping`, `local`, `responsive`, `lazy`, `quality`, `fm`, `fit`, `crop`, `imageWidth`, `imageHeight` |
| `Notification` | TwigComponent | `@components/Notification.html.twig` | Notification affichable par le front | `message`, `level` |
| `announcement:Banner` | TwigComponent | `@components/announcement/Banner.html.twig` | Bandeau d'annonces actives | Aucune prop publique |
| `announcement:Modal` | LiveComponent | `@components/announcement/Modal.html.twig` | Modal d'annonces actives | `template` si surcharge par instance, etat interne `open` |
| `collection:ProductCollection` | TwigComponent | `@components/collection/ProductCollection.html.twig` | Collection administrable de produits | `collection`, `title`, `limit` |
| `collection:AwardList` | LiveComponent | `@components/collection/AwardList.html.twig` | Liste de recompenses d'un produit avec ouverture de modal | `product`, `template` |
| `legal:Page` | TwigComponent | `@components/legal/Page.html.twig` | Page legale generique pour les mentions legales, les CGV ou la confidentialite | `type`, `template` |
| `marketing:ProductReviews` | TwigComponent | `@components/marketing/ProductReviews.html.twig` | Avis publics vérifiés d’une fiche produit, affichés uniquement quand les avis sont activés | `product` |
| `marketing:ProductReviewForm` | LiveComponent | `@components/marketing/ProductReviewForm.html.twig` | Depot d’un avis verifie depuis une invitation personnelle | `token`, etat interne `saved`, `template` |
| `marketing:ReviewSolicitationOptOut` | LiveComponent | `@components/marketing/ReviewSolicitationOptOut.html.twig` | Opposition aux futures sollicitations d’avis | `token`, etat interne `saved`, `template` |
| `news:Detail` | TwigComponent | `@components/news/Detail.html.twig` | Detail d'une actualite avec lien de retour | `news`, `slug`, `backRoute`, `template` |
| `news:SummaryList` | TwigComponent | `@components/news/SummaryList.html.twig` | Liste paginee de resumes d'actualites | `limit`, `page`, `pageParameterName`, `excerptLength`, `detailRoute`, `routeParameter`, `categorySlug`, `template` |
| `seo:SiteStructuredData` | TwigComponent | `@components/seo/SiteStructuredData.html.twig` | Schema.org generique du site et de la page courante | `pageTitle`, `homepageTitle`, `includeBreadcrumb` |
| `shop:Catalog` | TwigComponent | `@components/shop/Catalog.html.twig` | Catalogue boutique reutilisable, par boutique, categorie ou collection | `source`, `categorySlug`, `collection`, `showTabs`, `route`, `routeParam`, `frame`, `template` |
| `shop:Tabs` | TwigComponent | `@components/shop/Tabs.html.twig` | Navigation categorie boutique compatible Turbo Frame | `categories`, `activeSlug`, `route`, `routeParam`, `frame` |
| `shop:ProductGrid` | TwigComponent | `@components/shop/ProductGrid.html.twig` | Grille produits avec formulaires `cart:ItemForm` | `products`, `category`, `showLoginWhenEmpty` |
| `shop:CatalogSeo` | TwigComponent | `@components/shop/CatalogSeo.html.twig` | Schema.org du catalogue boutique | `source`, `categorySlug`, `collection` |
| `shop:CatalogMobileBar` | TwigComponent | `@components/shop/CatalogMobileBar.html.twig` | Barre mobile de navigation produits du catalogue | `source`, `categorySlug`, `collection` |
| `shop:ProductDetail` | TwigComponent | `@components/shop/ProductDetail.html.twig` | Detail visible d'un produit avec formulaire detail et mentions legales | `product`, `category` |
| `shop:ProductNavigation` | TwigComponent | `@components/shop/ProductNavigation.html.twig` | Navigation contextuelle entre la fiche produit et sa categorie | `product` |
| `shop:ProductCard` | LiveComponent | `@components/shop/ProductCard.html.twig` | Carte produit avec formulaire d'ajout panier | `product`, `template` |
| `shop:ProductDetailForm` | LiveComponent | `@components/shop/ProductDetailForm.html.twig` | Formulaire detail produit avec ajout panier | `product`, `template` |
| `shop:InformationBlocks` | LiveComponent | `@components/shop/InformationBlocks.html.twig` | Blocs d'information boutique, avec modals possibles | `zone`, `template` |
| `shop:LegalNotice` | TwigComponent | `@components/shop/LegalNotice.html.twig` | Mentions legales boutique | Aucune prop publique |
| `shop:PartnerCode` | LiveComponent | `components/shop/PartnerCode.html.twig` | Code partenaire ou groupe tarifaire | `code`, `template` |
| `shop:Vacation` | TwigComponent | `@components/shop/Vacation.html.twig` | Information de fermeture temporaire de la boutique | `config`, `template` |
| `cart:ItemForm` | LiveComponent | `@components/cart/ItemForm.html.twig` | Formulaire generique d'ajout au panier | `product`, `template` |
| `cart:CartForm` | LiveComponent | `@components/cart/CartForm.html.twig` | Panier, quantites et suppression de lignes | `cart`, `template` |
| `cart:CheckoutForm` | LiveComponent | `@components/cart/CheckoutForm.html.twig` | Tunnel checkout, livraison, promo, CGV et paiement | `cart`, `cartId`, `promoCodeInput`, `suppressDeliveryAutofill`, `suppressInvoiceAutofill`, `disableUserAutofill`, `template` |
| `payment:Form` | LiveComponent | `@components/payment/Form.html.twig` | Formulaire de paiement autonome pour facture ou lien de paiement | `paymentId`, `template` |
| `site:AgeGate` | LiveComponent | `@components/site/AgeGate.html.twig` | Contrôle de l'âge légal selon le pays, avec sélection de langue | `countryCode`, `birthYear`, `pageRoute`, `pageRouteParameters`, `template` |
| `site:ContactForm` | LiveComponent | `@components/site/ContactForm.html.twig` | Formulaire de contact | `template` |
| `site:Footer` | TwigComponent du bundle de theme | `components/site/Footer.html.twig` | Pied de page du theme, avec template de repli VitiCore | Aucune prop publique ; contact expose par `this.contact` |
| `security:LoginForm` | LiveComponent | `@components/security/LoginForm.html.twig` | Connexion utilisateur | `targetPath`, `template` |
| `security:RegisterForm` | LiveComponent | `@components/security/RegisterForm.html.twig` | Premiere etape d'inscription | `template` |
| `security:RegisterConfirmForm` | LiveComponent | `@components/security/RegisterConfirmForm.html.twig` | Confirmation d'inscription et mot de passe | `email`, `marketingOptIn`, `template` |
| `security:ForgotPasswordForm` | LiveComponent | `@components/security/ForgotPasswordForm.html.twig` | Demande de reinitialisation de mot de passe | `template` |
| `security:ResetPasswordForm` | LiveComponent | `@components/security/ResetPasswordForm.html.twig` | Reinitialisation de mot de passe | `token`, `template` |
| `depot` | LiveComponent | `@components/depot/Locator.html.twig` | Localisateur de depots avec filtres | `selectedCategoryId`, `selectedCountry`, `selectedDepotId`, `sidebarOpen`, `template` |
| `modal:SimpleModalProductDetail` | LiveComponent | `@components/modal/SimpleModalProductDetail.html.twig` | Modal detail produit, ouverte par evenement LiveComponent | `template`, etat interne de modal |
| `modal:SimpleModalAward` | LiveComponent | `@components/modal/SimpleModalAward.html.twig` | Modal recompense produit, ouverte par evenement LiveComponent | `template`, etat interne de modal |
| `modal:SimpleModalDegressivity` | LiveComponent | `@components/modal/SimpleModalDegressivity.html.twig` | Modal degressivite produit, ouverte par evenement LiveComponent | `template`, etat interne de modal |
| `modal:ShopInformation` | LiveComponent | `@components/modal/ShopInformation.html.twig` | Modal liee aux blocs d'information boutique | `template`, `selectedCountryFilter`, `selectedTypeFilter`, etat interne de modal |
| `social:InstagramMedias` | TwigComponent | `@components/social/InstagramMedias.html.twig` | Grille de medias Instagram configures | `limit`, `template` |
| `tracking:Scripts` | TwigComponent | `@components/tracking/Scripts.html.twig` | Scripts de tracking configures en admin | `position` |

## Routes VitiCore

Le loader `type: viti-core` charge seulement les routes techniques communes du coeur : routing interne, medias, sitemap, API Platform, FOS JS Routing, LiipImagine, UX LiveComponent et routes de debug en environnement `dev`. Ces routes ne sont pas des pages publiques a activer au cas par cas.

Les pages publiques restent optionnelles et doivent etre importees explicitement dans le projet hote, selon le site :

| Fichier | Role |
| --- | --- |
| `site_shop.yaml` | Boutique, fiche produit, checkout et confirmation de commande |
| `site_legal.yaml` | Mentions legales, politique de confidentialite et CGV via `TemplateController` |
| `site_depot.yaml` | Page publique des depots via le composant `<twig:depot />` |
| `site_news.yaml` | Pages publiques actualites si le site les expose |
| `site_security.yaml` | Connexion, inscription et compte utilisateur |
| `site_locale.yaml` | Changement de langue |
| `site_social.yaml` | Routes publiques liees aux reseaux sociaux si la fonctionnalite est utilisee |
| `site_payment.yaml` | Page autonome `/paiement-en-ligne` avec le composant `<twig:payment:Form />` |

Les routes publiques localisees du coeur declarent les variantes `fr`, `en`, `de`, `nl`, `it` et `es` pour que Symfony expose les noms `route.fr`, `route.en`, `route.de`, `route.nl`, `route.it` et `route.es`. La presence d'une route ne suffit pas a rendre la langue disponible : `LocaleListener` verifie la table `locale_locale` et renvoie une 404 si la locale demandee n'est pas active. La locale `fr` reste la locale par defaut du coeur.

Les routes techniques sans page publique, par exemple `/cdn`, `/request/*/responseAt`, `/api/*` ou les callbacks bancaires, ne sont pas localisees.

Les routes des moyens de paiement sont separees de la page autonome :

| Fichier | Role |
| --- | --- |
| `site_payment_methods.yaml` | Endpoints `payment_*` et callbacks `/request/*/responseAt` utilises par le checkout et par `payment:Form` |

Donc un site peut utiliser le checkout et les endpoints bancaires sans exposer `/paiement-en-ligne`. Dans ce cas, importer `site_shop.yaml` et `site_payment_methods.yaml`, mais pas `site_payment.yaml`.

Exemple :

```yaml
viti_core_site_shop:
    resource: '@CpCreationVitiCoreBundle/Resources/config/routes/site_shop.yaml'

viti_core_site_payment_methods:
    resource: '@CpCreationVitiCoreBundle/Resources/config/routes/site_payment_methods.yaml'
```

Importer `site_payment.yaml` uniquement si le site doit proposer une page de paiement autonome, par exemple pour regler une facture ou un lien de paiement hors panier.

La page publique des depots n'a pas besoin de controller dedie. La route prete `depots_index`, disponible via `src/Bridge/Symfony/Bundle/Resources/config/routes/site_depot.yaml`, rend `depots.html.twig` avec `TemplateController`, puis le template inclut le composant `<twig:depot />`.

## Composants catalogue boutique

Les routes pretes VitiCore restent optionnelles via le bloc `site_shop`, mais le rendu catalogue est maintenant pilotable depuis des composants. La route `shop` rend `shop/index.html.twig` avec `TemplateController`. Le template de reference inclut seulement le hero, `shop:Catalog`, `shop:CatalogSeo` et `shop:CatalogMobileBar`.

Le controleur `ShopController` ne porte plus la logique de listing catalogue. Il garde la fiche produit, pour la validation de visibilite, la redirection canonical et le tracking `ViewContent`, puis les routes checkout et commande.

Un theme qui surcharge `shop/index.html.twig` ne doit plus attendre les anciennes variables de controller comme `config`, `categories`, `shopProducts` ou `shopProductGroups`. Avec `TemplateController`, ces donnees doivent venir des composants :

```twig
{% extends 'base.html.twig' %}

{% block meta %}
    {{ include('partials/seo.html.twig', {
        title_key: 'shop.title',
        description_key: 'shop.description',
        robots: 'index,follow'
    }) }}
    <twig:shop:CatalogSeo />
{% endblock %}

{% block main %}
    <twig:shop:Catalog />
{% endblock %}

{% block mobile_bottom_bar %}
    <twig:shop:CatalogMobileBar />
{% endblock %}
```

Dans un projet hote qui utilise un theme legacy, surcharger `shop/index.html.twig` dans `templates/` pour remplacer le template du theme vendor par un wrapper de composants :

```twig
{% extends '@CpCreationVitiCore/shop/index.html.twig' %}

{% block main %}
    <section id="shop" class="theme-page theme-shop-page">
        <twig:shop:Catalog />
    </section>
{% endblock %}

{% block mobile_bottom_bar %}
    <twig:shop:CatalogMobileBar />
{% endblock %}
```

Toutes les URLs publiques boutique pretes a l'emploi sont declarees au meme endroit :

```text
src/Bridge/Symfony/Bundle/Resources/config/routes/site_shop.yaml
```

Si un projet doit utiliser une base autre que `/boutique` ou `/en/shop`, ne pas modifier le vendor. Remplacer l'import `site_shop` cote projet hote par des routes projet gardant les memes noms :

- `shop`
- `shop_product`
- `shop_checkout`
- `shop_checkout_cart`
- `shop_checkout_payment_pending`
- `shop_checkout_payment_unconfirmed`
- `shop_checkout_success`
- `shop_checkout_success_register`
- `shop_purchase_order_download`

Exemple de base personnalisee :

```yaml
shop:
    path:
        fr: /vente/{slug}
        en: /en/store/{slug}

shop_product:
    path:
        fr: /vente/{categorySlug}/{slug}
        en: /en/store/{categorySlug}/{slug}
```

Les composants utilisent les noms de routes, pas des URLs en dur. Un changement coherent des chemins dans `site_shop.yaml` met donc a jour les liens de catalogue, fiche produit, canonical et checkout.

### `shop:Catalog`

`shop:Catalog` est le composant principal de listing boutique. Il centralise l'affichage du catalogue et s'appuie sur `ShopCatalogResolver`.

Classe :

```text
src/Bridge/Symfony/Bundle/Twig/Components/Shop/Catalog.php
```

Template :

```text
src/Bridge/Symfony/Bundle/Resources/views/components/shop/Catalog.html.twig
```

Props :

| Prop | Type | Defaut | Role |
| --- | --- | --- | --- |
| `source` | `shop`, `category` ou `collection` | `shop` | Source de produits a afficher |
| `categorySlug` | `string|null` | slug de la route courante | Categorie forcee pour `source="category"` ou `source="shop"` |
| `collection` | `string|null` | `null` | Code administrable de `ProductCollection` pour `source="collection"` |
| `showTabs` | `bool` | `true` | Active ou masque les onglets quand plusieurs categories sont disponibles |
| `route` | `string` | `shop` | Route utilisee par les onglets |
| `routeParam` | `string` | `slug` | Nom du parametre route pour le slug categorie |
| `frame` | `string` | `shop-content` | Id du Turbo Frame mis a jour par les onglets |
| `template` | `string|null` | `null` | Template ponctuel a inclure a la place du rendu standard |

Modes :

- `source="shop"` : comportement boutique standard. Si `showCategoryMenuFront` est actif, le composant affiche la categorie active et les onglets. Sinon, il affiche les produits groupes par categorie.
- `source="category"` : force un rendu de categorie, meme si le menu categorie global est desactive. Utiliser `categorySlug`.
- `source="collection"` : affiche les produits d'une collection administrable. Les onglets sont ignores.

Exemples :

```twig
<twig:shop:Catalog />

<twig:shop:Catalog
    source="category"
    categorySlug="brut"
    :showTabs="false"
/>

<twig:shop:Catalog
    source="collection"
    collection="homepage-cuvees"
    :showTabs="false"
/>
```

Le rendu standard inclut `shop:PartnerCode`, `shop:InformationBlocks`, `shop:Tabs`, `shop:ProductGrid` et `shop:LegalNotice`. En cas de surcharge, conserver le `<turbo-frame id="{{ frame }}">`, `data-scroll-target="shopFrame"` et le bloc `data-mobile-bar-source`, car ils alimentent les onglets et la barre mobile.

### `shop:Tabs`

`shop:Tabs` rend la navigation des categories boutique. Le composant est serveur, mais les liens restent compatibles Turbo.

Props :

| Prop | Type | Defaut | Role |
| --- | --- | --- | --- |
| `categories` | `ProductCategory[]` | `[]` | Categories visibles boutique |
| `activeSlug` | `string` | `''` | Slug actif pour l'etat visuel |
| `route` | `string` | `shop` | Route cible des liens |
| `routeParam` | `string` | `slug` | Parametre route recevant le slug categorie |
| `frame` | `string` | `shop-content` | Turbo Frame cible |

Contrat fonctionnel a conserver dans une surcharge :

- `data-controller="shop-tabs"` ;
- `data-shop-tabs-link` ;
- `data-turbo-frame="{{ frame }}"` quand `frame` est renseigne ;
- `href="{{ path(route, this.routeParameters(category)) }}"` ;
- les attributs Alpine `x-data`, `x-on:click` et `:class` si le theme garde l'etat actif cote client ;
- la structure Splide si le projet utilise le carousel d'onglets.

### `shop:ProductGrid`

`shop:ProductGrid` affiche une liste de produits avec les formulaires panier existants. Il ne remplace pas `cart:ItemForm`; il l'orchestre.

Props :

| Prop | Type | Defaut | Role |
| --- | --- | --- | --- |
| `products` | `Product[]` | `[]` | Produits a afficher |
| `category` | `ProductCategory|null` | `null` | Categorie utilisee pour le message vide |
| `showLoginWhenEmpty` | `bool` | `true` | Affiche le lien de connexion groupe quand aucun produit ne correspond |

En cas de surcharge, garder l'appel :

```twig
{{ component('cart:ItemForm', { product: product }) }}
```

Le composant gere aussi le message `category.noGroupMatchMessage` et le lien vers `app_login` avec `_target_path`.

### `seo:SiteStructuredData`

`seo:SiteStructuredData` produit le graphe JSON-LD generique `WebSite`, `WebPage` et `BreadcrumbList`. Il relie ces schemas au `Winery` global injecte par VitiCore a partir de Contact.

```twig
{{ component('seo:SiteStructuredData', {
    pageTitle: head_title|striptags|trim,
    homepageTitle: 'homepage.title'|trans({}, 'meta')
}) }}
```

Le composant doit etre appele une seule fois dans le `<head>`. `WebPage` et `BreadcrumbList` ne sont pas generes sur la route canonique `homepage`. `includeBreadcrumb` peut etre passe a `false` lorsqu'une page fournit deja un fil d'Ariane plus precis.

### `shop:CatalogSeo`

`shop:CatalogSeo` extrait le JSON-LD du listing boutique.

Props :

| Prop | Type | Defaut | Role |
| --- | --- | --- | --- |
| `source` | `shop`, `category` ou `collection` | `shop` | Source catalogue a decrire |
| `categorySlug` | `string|null` | slug de la route courante | Categorie forcee |
| `collection` | `string|null` | `null` | Code de collection produit |

Il doit etre appele dans le block `meta` :

```twig
{% block meta %}
    {{ include('partials/seo.html.twig', {
        title_key: 'shop.title',
        description_key: 'shop.description',
        robots: 'index,follow'
    }) }}
    <twig:shop:CatalogSeo />
{% endblock %}
```

### `shop:CatalogMobileBar`

`shop:CatalogMobileBar` rend la barre mobile de selection rapide des produits. Elle lit les memes donnees que `shop:Catalog`.

Props :

| Prop | Type | Defaut | Role |
| --- | --- | --- | --- |
| `source` | `shop`, `category` ou `collection` | `shop` | Source catalogue |
| `categorySlug` | `string|null` | slug de la route courante | Categorie forcee |
| `collection` | `string|null` | `null` | Code de collection produit |

Contrat fonctionnel a conserver :

- `data-scroll-target="mobileBar"` ;
- `data-scroll-target="mobileLabel"` ;
- `data-scroll-target="mobileSelect"` ;
- `data-action="change->scroll#selectSection"` ;
- les valeurs `<option value="{{ product.slug }}">`.

### `shop:ProductDetail`

`shop:ProductDetail` rend la partie visible d'une fiche produit. La route `shop_product` reste controlee par `ShopController` pour gerer la 404, la redirection canonical et le tracking.

Props :

| Prop | Type | Role |
| --- | --- | --- |
| `product` | `Product` | Produit visible |
| `category` | `ProductCategory` | Categorie canonical du produit |

Exemple :

```twig
<twig:shop:ProductDetail :product="product" :category="category" />
```

Le rendu standard appelle `shop:ProductDetailForm` et `shop:LegalNotice`. En surcharge, garder `shop:ProductDetailForm` pour ne pas casser l'ajout panier et les modals produit.

### `collection:ProductCollection`

`collection:ProductCollection` remplace `collection:VariantCollection`. La collection est administree par code et contient des produits, pas des variants.

Props :

| Prop | Type | Defaut | Role |
| --- | --- | --- | --- |
| `collection` | `string` | `''` | Code administrable de la collection |
| `title` | `string|null` | nom de la collection | Titre affiche |
| `limit` | `int|null` | limite de la collection | Limite ponctuelle du rendu |

Exemple :

```twig
<twig:collection:ProductCollection collection="homepage-cuvees" />
```

Les collections automatiques sont limitees aux criteres produit : categorie, cuvee, nouveaute, rupture et limite.
Une collection peut contenir des produits de plusieurs groupes tarifaires : au rendu, le composant ne conserve que ceux du groupe actif.

Templates disponibles sans classe PHP dediee :

| Template | Role |
| --- | --- |
| `@components/shop/_information_block_card.html.twig` | Fragment de carte interne utilise par `shop:InformationBlocks` |

## Composants des pages d’avis

La fiche produit peut afficher les avis publics vérifiés sous son contenu avec :

```twig
<twig:marketing:ProductReviews :product="product" />
```

Le composant ne rend rien lorsque les relances d’avis sont désactivées dans la
configuration Marketing. Lorsqu’elles sont activées, il affiche la moyenne, la
liste paginée des avis publiés, les réponses du commerçant et les liens de
signalement. Son template peut être surchargé globalement sous
`templates/components/marketing/ProductReviews.html.twig`.

La route `shop_review_invitation` rend l’enveloppe `marketing/review/invitation.html.twig`, puis délègue le formulaire à :

```twig
<twig:marketing:ProductReviewForm :token="token" />
```

La route `shop_review_solicitation_opt_out` rend l’enveloppe `marketing/review/opt_out.html.twig`, puis délègue la confirmation à :

```twig
<twig:marketing:ReviewSolicitationOptOut :token="token" />
```

Les deux composants sont des LiveComponents et exposent `template` comme `LiveProp`. Un thème peut donc fournir un template miroir global sous `templates/components/marketing/` ou personnaliser une seule instance :

```twig
<twig:marketing:ReviewSolicitationOptOut
    :token="token"
    template="components/marketing/ReviewSolicitationOptOutCompact.html.twig"
/>
```

Contrat à conserver :

- `{{ attributes }}` sur l’élément racine ;
- la prop `token` ;
- `data-action`, `data-live-action-param`, `data-model` et `data-loading` du template de référence ;
- les champs et le CSRF Symfony UX du formulaire d’avis ;
- les métadonnées et en-têtes `noindex`, `nofollow`, `noarchive`, `no-referrer` et `private, no-store` des pages à token.

Le dépôt d’avis appelle la `LiveAction` `submitReview`. L’opposition appelle la `LiveAction` `confirmOptOut` ; la route publique de cette page reste en lecture seule (`GET`) et ne doit pas réimplémenter un POST dans le thème.

## Surcharger un template

Les templates du coeur ne doivent pas etre modifies depuis un projet hote. Pour changer le rendu, creer un template miroir dans le dossier `templates/` du projet hote avec le meme chemin relatif.

Exemple pour la carte produit :

```text
vendor/cpcreation/viti-core/src/Bridge/Symfony/Bundle/Resources/views/components/shop/ProductCard.html.twig
```

se surcharge avec :

```text
templates/components/shop/ProductCard.html.twig
```

La surcharge peut changer le HTML, les classes CSS, les espacements, les icones et l'organisation visuelle. Elle ne doit pas supprimer le contrat fonctionnel du composant.

## Surcharge ponctuelle par prop `template`

Certains composants utilisent le trait `HasTemplateOverride` et exposent une prop `template`. Cela permet de changer le template d'une seule instance sans remplacer le rendu global.

Exemple :

```twig
<twig:shop:ProductCard
    :product="product"
    template="components/home/_featured_product_card.html.twig"
/>
```

Pour les LiveComponents qui exposent `template`, la prop est conservee pendant les re-rendus AJAX. Tous les composants ne supportent pas cette personnalisation par instance : verifier la classe PHP avant de l'utiliser.

## Regle critique pour les LiveComponents

Dans un template de `LiveComponent`, l'element racine doit rendre `{{ attributes }}` :

```twig
<div {{ attributes }}>
    ...
</div>
```

Cet attribut est obligatoire. Il branche le controleur Stimulus de Symfony UX LiveComponent et permet les re-rendus AJAX. Sans lui, les `LiveAction`, `LiveProp`, `data-model` et etats de chargement ne fonctionneront plus.

Lors d'une surcharge, conserver aussi les attributs fonctionnels presents dans le template d'origine :

- `data-action="live#action"` et ses variantes comme `live#action:prevent` ;
- `data-live-action-param` ;
- `data-live-*-param` pour les arguments de `LiveAction` ;
- `data-model` pour les champs lies aux `LiveProp` ou aux formulaires LiveComponent ;
- `data-loading`, `data-live-ignore`, `data-live-preserve` si presents ;
- les `form_start()` et leurs attributs `data-action`, `data-model`, `method`, `action`, `novalidate` ;
- les champs Symfony Form, leurs `name`, `id`, valeurs et champs CSRF ;
- les hooks Stimulus : `data-controller`, `data-action`, `data-*-target`, `data-*-value`, `data-*-class`, `data-*-outlet` ;
- les hooks Alpine : `x-data`, `x-show`, `x-model`, `x-cloak`, `x-on:*`, `@click`, `@keydown`, `:class` ;
- les hooks Turbo : `<turbo-frame>`, `<turbo-stream>`, `data-turbo-frame`, `data-turbo-action`, `data-turbo-permanent`, `data-turbo-cache`.

Une surcharge sure commence par copier la structure fonctionnelle du template d'origine, puis adapte progressivement les classes et le markup de presentation.

Quand un template surcharge un LiveComponent base sur un formulaire Symfony dynamique, verifier l'existence des champs avant de lire leurs variables ou de les rendre. Certains champs peuvent disparaitre apres un re-rendu AJAX selon le variant, l'option ou l'etape courante.

```twig
{% if form.quantity is defined %}
    {{ form_widget(form.quantity) }}
{% endif %}
```

Cette precaution concerne notamment `form.quantity`, `form.option` et les champs ajoutes par `DynamicFormBuilder`.

## Points d'attention par famille

### Boutique et panier

`shop:ProductCard`, `shop:ProductDetailForm` et `cart:ItemForm` heritent du meme comportement d'ajout panier. Ils utilisent un formulaire Symfony LiveComponent, des actions comme `save`, `showDegressiveModal`, `showAwardModal` et `showProductDetailModal`, et peuvent emettre des evenements vers les modals.

Lors d'une surcharge, conserver le `form_start(form, ...)`, les champs `form.*`, les `data-model`, les boutons `data-action="live#action"` et les `data-live-*-param`.
Pour les champs conditionnels, utiliser un garde comme `{% if form.quantity is defined %}` avant `form.quantity.vars` ou `form_widget(form.quantity)`.

`cart:CartForm` ecoute l'evenement `cartUpdated`. `cart:CheckoutForm` gere plusieurs etapes du checkout. Ne pas les remplacer par une logique projet equivalente : adapter le template, mais garder le composant VitiCore.

### Collections administrables

Pour afficher des produits administres, utiliser en priorite :

```twig
<twig:collection:ProductCollection collection="homepage-cuvees" />
```

Le code de collection est administrable. Ne pas cibler une collection par ID, par position, par premier resultat trouve ou par nom affiche.

Pour afficher le catalogue boutique complet ou une source precise :

```twig
<twig:shop:Catalog />

<twig:shop:Catalog source="category" categorySlug="brut" :showTabs="false" />

<twig:shop:Catalog source="collection" collection="homepage-cuvees" :showTabs="false" />
```

### Modals

Les modals VitiCore sont souvent ouvertes par des evenements emis depuis d'autres LiveComponents. Par exemple, une carte produit peut emettre vers `modal:SimpleModalProductDetail`.

Il faut donc garder les composants modals presents dans le layout ou dans la page lorsque les boutons correspondants existent dans le rendu.

### Image

Le composant `Image` accepte des attributs imbriques, par exemple :

```twig
<twig:Image
    path="{{ product.image.path|image_path }}"
    alt="{{ product.name }}"
    class="h-full w-full"
    img:class="h-full w-full object-cover"
/>
```

Dans une surcharge de son template, conserver les usages de `attributes`, `attributes.nested('img')`, `attributes.nested('picture')` et `attributes.nested('row')`, sinon les classes passees par les projets hotes ne seront plus appliquees.

### Tracking

`tracking:Scripts` depend de la configuration boutique et de la position demandee :

```twig
<twig:tracking:Scripts position="head" />
<twig:tracking:Scripts position="body" />
```

Ne pas dupliquer les scripts en dur dans les templates projet si le composant couvre deja le besoin.

## Checklist avant de terminer une surcharge

Avant de valider une surcharge de template VitiCore :

1. Comparer le template surcharge avec le template source.
2. Verifier que `{{ attributes }}` est conserve sur la racine des composants Twig et LiveComponent.
3. Verifier que les attributs `data-*`, Alpine, Stimulus, Turbo et Symfony Form utiles sont toujours presents.
4. Verifier qu'aucune logique metier du coeur n'a ete copiee dans le projet hote.
5. Verifier que les composants VitiCore existants sont reutilises au lieu de recréer une logique equivalente.
6. Verifier que les champs de formulaire dynamiques sont testes avec `is defined` avant usage.
7. Lancer les verifications Twig depuis le conteneur applicatif du projet hote.

Commande habituelle dans un projet hote avec Docker :

```bash
docker compose exec php php bin/console lint:twig templates
docker compose exec php php bin/console cache:clear
```

Pour une modification explicite du coeur :

```bash
docker compose exec php php bin/console lint:twig vendor/cpcreation/viti-core/src/Bridge/Symfony/Bundle/Resources/views
docker compose exec php php bin/console cache:warmup --env=dev
```
