# @altazion/commerce-sdk-htmx

Extension HTMX et helpers Handlebars pour l'Altazion Commerce SDK. Permet d'intégrer le SDK dans des projets à rendu serveur utilisant [htmx](https://htmx.org) et/ou [Handlebars](https://handlebarsjs.com), sans framework JavaScript.

## Installation

```bash
npm install @altazion/commerce-sdk-htmx @altazion/commerce-sdk-core
npm install htmx.org handlebars  # peer dependencies (optionnelles)
```

## Initialisation

```ts
import { CommerceClient } from '@altazion/commerce-sdk-core'
import { initAltazionHtmx } from '@altazion/commerce-sdk-htmx'
import Handlebars from 'handlebars'

const client = new CommerceClient({
  baseUrl: 'https://votre-api.altazion.com',
})

initAltazionHtmx({
  client,
  handlebars: Handlebars,     // optionnel — active les helpers HBS
  offlineSelector: '#app',    // optionnel — élément qui reçoit la classe altz-offline
  terminalMode: {
    interactiveSelectors: ['#app-main', '.altz-action-bar'],
    offlineScreenSelectors: '#offline-screen',
  },
})
```

## Mode CDN (sans bundler)

```html
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/handlebars/dist/handlebars.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@altazion/commerce-sdk-core/dist/index.iife.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@altazion/commerce-sdk-htmx/dist/index.iife.js"></script>
<script>
  const client = new AltazionCommerceCore.CommerceClient({
    baseUrl: 'https://votre-api.altazion.com'
  })
  AltazionCommerceSdkHtmx.initAltazionHtmx({ client, handlebars: Handlebars })
</script>
```

## Helpers Handlebars

### Prix

| Helper | Description | Exemple |
|---|---|---|
| `{{formatPrice value}}` | Prix formaté selon la locale | `12,90 €` |
| `{{formatPriceNoSymbol value}}` | Prix sans symbole monétaire | `12,90` |

### Produit

| Helper | Description |
|---|---|
| `{{productUrl sku}}` | URL canonique d'un produit |
| `{{productImageUrl url size}}` | URL d'image redimensionnée |

### Panier

| Helper | Description |
|---|---|
| `{{cartItemCount}}` | Nombre total d'articles dans le panier |
| `{{cartTotal}}` | Total TTC formaté |
| `{{#cartHasItems}}...{{/cartHasItems}}` | Bloc conditionnel si panier non vide |
| `{{cartLineCount}}` | Nombre de lignes dans le panier |

## Templates pré-compilés

Le package expose des templates Handlebars prêts à l'emploi accessibles via :

```ts
import { templates } from '@altazion/commerce-sdk-htmx'

// templates.productCard    — carte produit
// templates.cartMini       — mini panier
// templates.productList    — liste de produits
```

## Extension HTMX Altazion

L'initialisation enregistre automatiquement une extension HTMX `altazion` qui :

- injecte les headers de session sur les requêtes HTMX ;
- interprète les attributs `hx-altazion-cart-action` pour appeler directement `client.cart.*` du SDK core ;
- interprète les attributs `hx-altazion-session-action` pour appeler directement `client.session.*` du SDK core ;
- interprète les attributs `hx-altazion-marketing-action` pour appeler directement `client.marketing.*` du SDK core ;
- interprète les attributs `hx-altazion-stores-action` pour appeler directement `client.stores.*` du SDK core ;
- émet des événements DOM `altazion:cart:*` exploitables par HTMX pour recharger des fragments.

### Convention de nommage

La convention officielle du DSL HTMX Altazion est la suivante :

- activation : `hx-ext="altazion"` ;
- action métier : `hx-altazion-<module>-action` ;
- paramètres simples : `hx-altazion-<module>-<parametre-kebab-case>` ;
- payload riche : formulaire ou `hx-vals` ;
- refresh partagé : `hx-altazion-refresh` ;
- événements : `altazion:<module>:before|after|error`, avec des événements complémentaires selon le module (`updated`, `loaded`, `changed`, etc.).

La convention détaillée est décrite dans [specs-temp/sdk.htmx-naming-convention.md](specs-temp/sdk.htmx-naming-convention.md).

### Exemple addItem

```html
<div hx-ext="altazion">
  <button
    hx-altazion-cart-action="addItem"
    hx-vals='{"reference":"REF-001","quantity":1}'
    hx-altazion-refresh="#cart-mini; #cart-badge"
  >
    Ajouter au panier
  </button>

  <aside
    id="cart-mini"
    hx-get="/fragments/cart-mini"
    hx-swap="outerHTML"
  ></aside>
</div>
```

### Actions supportées

| Action | Méthode SDK | Payload attendu |
|---|---|---|
| `getCart` | `client.cart.getCart()` | aucun |
| `getValidationStatus` | `client.cart.getValidationStatus()` | aucun |
| `addItem` | `client.cart.addItem(reference, quantity, options?)` | `reference`, `quantity` |
| `updateItem` | `client.cart.updateItem(lineId, quantity)` | `lineId`, `quantity` |
| `removeItem` | `client.cart.removeItem(lineId)` | `lineId` |
| `applyCoupon` | `client.cart.applyCoupon(code)` | `code` |
| `removeCoupon` | `client.cart.removeCoupon(code)` | `code` |

Le payload peut être fourni via `hx-vals`, via les champs d'un formulaire, ou avec les attributs dédiés `hx-altazion-cart-reference`, `hx-altazion-cart-quantity`, `hx-altazion-cart-line-id`, `hx-altazion-cart-code`.

### Session: lecture de la session courante

Pour les sites en server rendering, la session peut déjà exister côté serveur et être portée par cookie. Le premier besoin côté HTMX est donc souvent simplement de relire cet état pour rafraîchir des fragments.

```html
<section
  hx-ext="altazion"
  hx-altazion-session-action="getSession"
  hx-trigger="load, altazion:session:refresh from:body"
  hx-altazion-refresh="#header-session; #mini-cart"
>
</section>

<header id="header-session" hx-get="/fragments/header-session" hx-swap="outerHTML"></header>
<aside id="mini-cart" hx-get="/fragments/cart-mini" hx-swap="outerHTML"></aside>
```

Action supportée pour l'instant :

| Action | Méthode SDK | Payload attendu |
|---|---|---|
| `getSession` | `client.session.getSession()` | aucun |

Événements émis :

| Événement | Description |
|---|---|
| `altazion:session:before` | avant l'appel au SDK |
| `altazion:session:after` | après un appel réussi |
| `altazion:session:loaded` | après `getSession` |
| `altazion:session:error` | en cas d'erreur |

### Rendu déclaratif inline

Quand `handlebars` est fourni à `initAltazionHtmx`, le package sait aussi piloter un rendu inline déclaratif dans le DOM.

Le pattern visé est :

```html
<section
  hx-ext="altazion"
  hx-altazion-marketing-action="getItems"
  hx-altazion-marketing-codes='["HOMEPAGEHERO"]'
  hx-altazion-data-slot="homepageHero"
></section>

<aside id="store-detail" class="panel">
  <p
    hx-altazion-data-source="slot:homepageHero"
    hx-altazion-data-status="null"
    class="placeholder"
  >
    Aucun contenu marketing chargé.
  </p>

  <div
    hx-altazion-data-source="slot:homepageHero"
    hx-altazion-data-status="not-empty"
    hx-render-template="homepage-hero-template"
  ></div>
</aside>

<script id="homepage-hero-template" type="text/x-handlebars-template">
  {{#each slot}}
  <article>
    <h2>{{linkItem.altData}}</h2>
  </article>
  {{/each}}
</script>
```

Attributs actuellement pris en charge :

- `hx-altazion-data-source="cart|session|store|stores|marketing-item|marketing-items"`
- `hx-altazion-data-source="slot:nom-libre"`
- `hx-altazion-data-status="null|not-null|empty|not-empty"`
- `hx-altazion-data-slot="nom-libre"` sur un élément d'action Altazion pour ranger son résultat dans un slot nommé
- `hx-render-template="template-id"`

Le contexte Handlebars exposé au template contient aujourd'hui : `cart`, `session`, `store`, `stores`, `marketingItem`, `marketingItems`, `slots` et `slot`.

### Marketing: items de widgets et d'animation

Le module `marketing` sert bien au cas d'usage widgets de page: récupérer un item marketing isolé ou un lot d'items, puis laisser HTMX rafraîchir les fragments de rendu serveur associés.

```html
<section
  hx-ext="altazion"
  hx-altazion-marketing-action="getItems"
  hx-altazion-marketing-codes='["home-hero", "home-sidebar", "home-footer"]'
  hx-trigger="load, altazion:marketing:refresh from:body"
  hx-altazion-refresh="#widget-hero; #widget-sidebar; #widget-footer"
>
</section>

<section id="widget-hero" hx-get="/fragments/widgets/hero" hx-swap="outerHTML"></section>
<aside id="widget-sidebar" hx-get="/fragments/widgets/sidebar" hx-swap="outerHTML"></aside>
<footer id="widget-footer" hx-get="/fragments/widgets/footer" hx-swap="outerHTML"></footer>
```

Actions supportées :

| Action | Méthode SDK | Payload attendu |
|---|---|---|
| `getItem` | `client.marketing.getItem(code)` | `code` |
| `getItems` | `client.marketing.getItems(codes)` | `codes` |

Le payload peut être fourni via `hx-vals`, via un formulaire, ou avec :

- `hx-altazion-marketing-code` pour un item unique ;
- `hx-altazion-marketing-codes` pour une liste JSON de codes ;
- une chaîne séparée par des virgules dans `codes` si c'est plus simple côté template.

Événements émis :

| Événement | Description |
|---|---|
| `altazion:marketing:before` | avant l'appel au SDK |
| `altazion:marketing:after` | après un appel réussi |
| `altazion:marketing:loaded` | après une lecture réussie |
| `altazion:marketing:item-loaded` | après `getItem` |
| `altazion:marketing:items-loaded` | après `getItems` |
| `altazion:marketing:error` | en cas d'erreur |

### Stores: store locator et détail magasin

Le domaine `stores` est un bon candidat HTMX parce qu'il est purement orienté lecture pour l'instant: localiser une liste de magasins ou charger la fiche d'un magasin pour un widget, une modale ou une page de retrait.

```html
<form
  hx-ext="altazion"
  hx-altazion-stores-action="findByPostalCode"
  hx-trigger="submit"
  hx-altazion-refresh="#store-results"
>
  <input type="text" name="postalCode" value="75002">
  <input type="hidden" name="countryCode" value="FR">
  <button type="submit">Chercher un magasin</button>
</form>

<section id="store-results" hx-get="/fragments/stores/results" hx-swap="outerHTML"></section>
```

```html
<button
  hx-ext="altazion"
  hx-altazion-stores-action="getStore"
  hx-altazion-stores-store-guid="STORE-GUID-001"
  hx-altazion-refresh="#store-detail"
>
  Voir ce magasin
</button>

<aside id="store-detail" hx-get="/fragments/stores/detail" hx-swap="outerHTML"></aside>
```

Actions supportées :

| Action | Méthode SDK | Payload attendu |
|---|---|---|
| `findByLocation` | `client.stores.findByLocation(latitude, longitude, radiusKm?)` | `latitude`, `longitude` |
| `findByPostalCode` | `client.stores.findByPostalCode(postalCode, countryCode?)` | `postalCode` |
| `getStore` | `client.stores.getStore(storeGuid)` | `storeGuid` |

Paramètres HTML pris en charge :

- `hx-altazion-stores-latitude`
- `hx-altazion-stores-longitude`
- `hx-altazion-stores-radius-km`
- `hx-altazion-stores-postal-code`
- `hx-altazion-stores-country-code`
- `hx-altazion-stores-store-guid`

Événements émis :

| Événement | Description |
|---|---|
| `altazion:stores:before` | avant l'appel au SDK |
| `altazion:stores:after` | après un appel réussi |
| `altazion:stores:loaded` | après une lecture réussie |
| `altazion:stores:stores-loaded` | après une recherche de magasins |
| `altazion:stores:store-loaded` | après `getStore` |
| `altazion:stores:error` | en cas d'erreur |

### Rafraîchissement déclaratif

L'attribut `hx-altazion-refresh` accepte une ou plusieurs cibles CSS séparées par `;`.

- si la cible porte un attribut `hx-get`, `hx-post`, `hx-put`, `hx-patch` ou `hx-delete`, l'extension force immédiatement la requête HTMX correspondante ;
- sinon l'extension émet `altazion:refresh` sur la cible, ce qui laisse la main à du rendu client ou à un autre binding HTMX.

```html
<button
  hx-ext="altazion"
  hx-altazion-cart-action="applyCoupon"
  hx-altazion-cart-code="WELCOME10"
  hx-altazion-refresh="#cart-mini; #cart-totals"
>
  Appliquer le coupon
</button>

<section id="cart-totals" hx-get="/fragments/cart-totals" hx-swap="outerHTML"></section>
```

### Événements émis

| Événement | Description |
|---|---|
| `altazion:cart:before` | avant l'appel au SDK |
| `altazion:cart:after` | après un appel réussi |
| `altazion:cart:loaded` | après une lecture réussie, par exemple `getCart` |
| `altazion:cart:updated` | après une action retournant un panier |
| `altazion:cart:changed` | après une action réussie sans payload de retour |
| `altazion:cart:status` | après `getValidationStatus` |
| `altazion:cart:error` | en cas d'erreur, avec `detail.errorDetail` normalisé |
| `altazion:refresh` | fallback émis sur une cible sans requête HTMX |

Le moteur HTMX générique applique désormais une sémantique commune :

- `loaded` pour les actions de lecture ;
- `updated` pour les mutations avec résultat métier ;
- `changed` pour les mutations qui réussissent sans retourner de payload ;
- `error` avec un objet `detail.errorDetail` structuré : `name`, `message`, `status`, `problem`, `isOffline`, `isConflict`.

## Détection offline

Lorsque la connexion est perdue, l'élément ciblé par `offlineSelector` (par défaut `body`) reçoit :

- la classe `altz-offline` ;
- la classe `altz-terminal-offline` ou `altz-terminal-online` ;
- l'attribut `data-altazion-terminal-state="offline|online"` ;
- les événements `altazion:connectivity:changed`, `altazion:offline` et `altazion:online`.

Le mode borne peut aussi masquer des zones interactives et afficher un écran passif via `terminalMode.interactiveSelectors` et `terminalMode.offlineScreenSelectors`.

### Exemple de bascule borne

```html
<body>
  <main id="app-main">
    <!-- interface interactive -->
  </main>

  <aside id="offline-screen" hidden>
    <!-- pub, branding, message d'attente -->
  </aside>
</body>
```

```css
body.altz-terminal-offline {
  background: #111;
  color: #fff;
}

#offline-screen[hidden] {
  display: none;
}
```

L'écran offline est affiché automatiquement quand la borne passe hors ligne, et les zones interactives ciblées sont masquées.

### Exemple d'écoute des événements

```ts
document.body.addEventListener('altazion:offline', () => {
  console.log('Le terminal est hors ligne')
})

document.body.addEventListener('altazion:online', () => {
  console.log('Le terminal est de nouveau en ligne')
})
```

Les mutations métier continuent d'échouer immédiatement hors ligne avec `OfflineError`. Le mode offline borne ne réintroduit aucune synchronisation différée.

### Exemple CSS minimal

Lorsque seule l'indication visuelle suffit, la classe `altz-offline` permet d'afficher un bandeau ou de masquer des actions :

```css
body.altz-offline .altz-cart-actions {
  opacity: 0.5;
  pointer-events: none;
}
```

## Licence

Propriétaire — © Altazion SAS. Tous droits réservés.
