Aller au contenu

⏳ Comptes à reboursGratuit · 0 NahnahPar NAHNAH AI

Cartes effet verre

Cartes translucides façon verre dépoli sur un fond de couleurs floues qui dérivent lentement, chiffres nets et bouton. Fond immobile si le visiteur réduit les animations.

Aperçu en direct

Données de démonstration (produits, menus, textes d’exemple) : dans ta boutique, ce sont les tiennes.

Photos : Unsplash / Pexels (exemples de l’aperçu, jamais inclus dans le code).

Code à copier

Fichier complet sections/nahnah-rebours-cartes-verre.liquid (Liquid, CSS et JS inclus).

{%- comment -%} NAHNAH AI · Cartes effet verre (bibliothèque Sections & effets) {%- endcomment -%}
{%- if section.settings.date_fin != blank -%}
<nn-rebours-cartes-verre class="nn-rebours-cartes-verre" style="--nn-mh: {{ section.settings.marge_haut }}px; --nn-mb: {{ section.settings.marge_bas }}px; --nn-fond: {{ section.settings.couleur_fond }}; --nn-texte: {{ section.settings.couleur_texte }}; --nn-accent: {{ section.settings.couleur_accent }}; --nn-bouton: {{ section.settings.couleur_bouton }}; --nn-texte-bouton: {{ section.settings.couleur_texte_bouton }};" data-mode="date" data-zone="{{ section.settings.fuseau | escape }}" data-libelles="{{ section.settings.libelles | escape }}" data-fin="{{ section.settings.date_fin | escape }}" data-a-la-fin="{{ section.settings.a_la_fin }}"{% if request.design_mode %} data-editeur{% endif %}>
<div class="nn-rebours-cartes-verre__fond" aria-hidden="true" style="--nn-couleur-2: {{ section.settings.couleur_2 }};"><span class="nn-rebours-cartes-verre__tache"></span><span class="nn-rebours-cartes-verre__tache nn-rebours-cartes-verre__tache--b"></span></div>
<div class="nn-rebours-cartes-verre__contenu">
  <h2 class="nn-rebours-cartes-verre__titre">{{ section.settings.titre }}</h2>
  <div data-pendant>{%- assign nn_l = section.settings.libelles | split: ',' -%}
<div class="nn-rebours-cartes-verre__unites" role="timer" aria-live="off" aria-atomic="true">
  <div class="nn-rebours-cartes-verre__bloc" data-bloc-jours>
    <span class="nn-rebours-cartes-verre__nombre" data-unite="j">--</span>
    <span class="nn-rebours-cartes-verre__libelle">{{ nn_l[0] | strip | escape }}</span>
  </div>
  <div class="nn-rebours-cartes-verre__bloc">
    <span class="nn-rebours-cartes-verre__nombre" data-unite="h">--</span>
    <span class="nn-rebours-cartes-verre__libelle">{{ nn_l[1] | strip | escape }}</span>
  </div>
  <div class="nn-rebours-cartes-verre__bloc">
    <span class="nn-rebours-cartes-verre__nombre" data-unite="m">--</span>
    <span class="nn-rebours-cartes-verre__libelle">{{ nn_l[2] | strip | escape }}</span>
  </div>
  <div class="nn-rebours-cartes-verre__bloc">
    <span class="nn-rebours-cartes-verre__nombre" data-unite="s">--</span>
    <span class="nn-rebours-cartes-verre__libelle">{{ nn_l[3] | strip | escape }}</span>
  </div>
</div></div>
  <p class="nn-rebours-cartes-verre__fin" data-apres hidden>{{ section.settings.message_fin }}</p>
  {%- if section.settings.lien != blank and section.settings.texte_bouton != blank -%}
    <a class="nn-rebours-cartes-verre__bouton" href="{{ section.settings.lien }}">{{ section.settings.texte_bouton | escape }}</a>
  {%- endif -%}
</div>
<span class="nn-rebours-cartes-verre__lecteur">Fin : <span data-echeance></span></span>
<span class="nn-rebours-cartes-verre__lecteur" role="status" data-annonce data-texte-fin="{{ section.settings.message_fin | strip_html | escape }}"></span>
<p class="nn-rebours-cartes-verre__alerte" data-alerte hidden>Date illisible : écris-la par exemple 24/12/2026 18:00 (message visible dans l’éditeur seulement).</p>
<p class="nn-rebours-cartes-verre__alerte" data-note-masque hidden>Date passée : la section est masquée pour les visiteurs (ce message n’apparaît que dans l’éditeur).</p>
</nn-rebours-cartes-verre>
{%- elsif request.design_mode -%}
  <p class="nn-rebours-cartes-verre__editeur">Indique la vraie date de fin de ton offre pour afficher le compte à rebours.</p>
{%- endif -%}

{% schema %}
{
  "name": "Rebours effet verre",
  "tag": "section",
  "class": "nn-section-rebours-cartes-verre",
  "settings": [
    {
      "type": "inline_richtext",
      "id": "titre",
      "label": "Titre",
      "default": "L’offre se termine bientôt"
    },
    {
      "type": "text",
      "id": "texte_bouton",
      "label": "Texte du bouton",
      "default": "En profiter"
    },
    {
      "type": "url",
      "id": "lien",
      "label": "Lien du bouton"
    },
    {
      "type": "color",
      "id": "couleur_2",
      "label": "Deuxième couleur du fond",
      "default": "#06B6D4"
    },
    {
      "type": "header",
      "content": "Fin de l’offre"
    },
    {
      "type": "text",
      "id": "date_fin",
      "label": "Date et heure de fin",
      "placeholder": "24/12/2026 18:00",
      "info": "Vraie date de fin de ton offre, à l’heure du fuseau choisi : 24/12/2026 18:00 ou 2026-12-24 18:00 (sans heure : 23:59). Le compte à rebours ne repart jamais à zéro."
    },
    {
      "type": "select",
      "id": "fuseau",
      "label": "Fuseau horaire des dates",
      "options": [
        {
          "value": "Europe/Paris",
          "label": "France, Belgique, Suisse, Luxembourg (Paris)"
        },
        {
          "value": "Europe/London",
          "label": "Royaume-Uni, Irlande (Londres)"
        },
        {
          "value": "Europe/Lisbon",
          "label": "Portugal (Lisbonne)"
        },
        {
          "value": "America/Toronto",
          "label": "Québec, Ontario (Montréal)"
        },
        {
          "value": "America/New_York",
          "label": "États-Unis, côte Est (New York)"
        },
        {
          "value": "America/Martinique",
          "label": "Martinique"
        },
        {
          "value": "America/Guadeloupe",
          "label": "Guadeloupe"
        },
        {
          "value": "America/Cayenne",
          "label": "Guyane"
        },
        {
          "value": "Indian/Reunion",
          "label": "La Réunion"
        },
        {
          "value": "Indian/Mayotte",
          "label": "Mayotte"
        },
        {
          "value": "Pacific/Noumea",
          "label": "Nouvelle-Calédonie"
        },
        {
          "value": "Pacific/Tahiti",
          "label": "Polynésie française (Tahiti)"
        },
        {
          "value": "Africa/Casablanca",
          "label": "Maroc (Casablanca)"
        },
        {
          "value": "Africa/Algiers",
          "label": "Algérie (Alger)"
        },
        {
          "value": "Africa/Tunis",
          "label": "Tunisie (Tunis)"
        },
        {
          "value": "Africa/Dakar",
          "label": "Sénégal (Dakar)"
        },
        {
          "value": "Africa/Abidjan",
          "label": "Côte d’Ivoire (Abidjan)"
        },
        {
          "value": "Africa/Douala",
          "label": "Cameroun (Douala)"
        },
        {
          "value": "Africa/Kinshasa",
          "label": "RD Congo (Kinshasa)"
        },
        {
          "value": "UTC",
          "label": "Temps universel (UTC)"
        }
      ],
      "default": "Europe/Paris"
    },
    {
      "type": "select",
      "id": "a_la_fin",
      "label": "Une fois la date passée",
      "options": [
        {
          "value": "message",
          "label": "Afficher le message de fin"
        },
        {
          "value": "masquer",
          "label": "Masquer la section"
        }
      ],
      "default": "message"
    },
    {
      "type": "inline_richtext",
      "id": "message_fin",
      "label": "Message de fin",
      "default": "Cette offre est terminée. Merci pour ta visite !"
    },
    {
      "type": "header",
      "content": "Affichage"
    },
    {
      "type": "text",
      "id": "libelles",
      "label": "Libellés des unités",
      "default": "jours, heures, minutes, secondes",
      "info": "Jours, heures, minutes, secondes : séparés par des virgules, dans cet ordre."
    },
    {
      "type": "header",
      "content": "Couleurs"
    },
    {
      "type": "color",
      "id": "couleur_fond",
      "label": "Fond",
      "default": "#0F172A"
    },
    {
      "type": "color",
      "id": "couleur_texte",
      "label": "Texte",
      "default": "#FFFFFF"
    },
    {
      "type": "color",
      "id": "couleur_accent",
      "label": "Accent",
      "default": "#8B5CF6"
    },
    {
      "type": "color",
      "id": "couleur_bouton",
      "label": "Bouton",
      "default": "#FFFFFF"
    },
    {
      "type": "color",
      "id": "couleur_texte_bouton",
      "label": "Texte du bouton",
      "default": "#0F172A"
    },
    {
      "type": "header",
      "content": "Espacement"
    },
    {
      "type": "range",
      "id": "marge_haut",
      "label": "Marge haute",
      "min": 0,
      "max": 120,
      "step": 4,
      "unit": "px",
      "default": 56
    },
    {
      "type": "range",
      "id": "marge_bas",
      "label": "Marge basse",
      "min": 0,
      "max": 120,
      "step": 4,
      "unit": "px",
      "default": 56
    }
  ],
  "presets": [
    {
      "name": "Rebours effet verre"
    }
  ]
}
{% endschema %}

{% stylesheet %}
.nn-rebours-cartes-verre {
  display: block;
  padding: var(--nn-mh, 0px) 16px var(--nn-mb, 0px);
  background: var(--nn-fond);
  color: var(--nn-texte);
}
.nn-rebours-cartes-verre[hidden], .nn-rebours-cartes-verre [hidden] { display: none !important; }
.nn-rebours-cartes-verre *, .nn-rebours-cartes-verre *::before, .nn-rebours-cartes-verre *::after { box-sizing: border-box; }
.nn-rebours-cartes-verre :focus-visible { outline: 3px solid var(--nn-accent); outline-offset: 3px; }
.nn-rebours-cartes-verre__editeur { margin: 0; padding: 16px; font-size: 14px; text-align: center; border: 1px dashed currentColor; opacity: 0.7; }
.nn-rebours-cartes-verre { position: relative; }
.nn-rebours-cartes-verre__lecteur { position: absolute !important; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
.nn-rebours-cartes-verre__alerte { max-width: 640px; margin: 12px auto 0; padding: 8px 12px; font-size: 13px; line-height: 1.4; text-align: center; border: 1px dashed currentColor; border-radius: 8px; opacity: 0.85; }
.nn-rebours-cartes-verre__fin { margin: 0; overflow-wrap: anywhere; }
.nn-rebours-cartes-verre [data-unite], .nn-rebours-cartes-verre [data-texte] { font-variant-numeric: tabular-nums; }
.nn-rebours-cartes-verre { overflow: hidden; isolation: isolate; text-align: center; }
.nn-rebours-cartes-verre__fond { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.nn-rebours-cartes-verre__tache { position: absolute; top: -20%; left: 5%; width: 55%; aspect-ratio: 1; border-radius: 50%; background: var(--nn-accent); filter: blur(60px); opacity: 0.7; animation: nn-rebours-cartes-verre-derive 14s ease-in-out infinite alternate; }
.nn-rebours-cartes-verre__tache--b { top: auto; bottom: -25%; left: auto; right: 0; background: var(--nn-couleur-2); animation-duration: 18s; animation-direction: alternate-reverse; }
@keyframes nn-rebours-cartes-verre-derive { to { transform: translate(18%, 12%) scale(1.1); } }
.nn-rebours-cartes-verre__contenu { position: relative; max-width: 860px; margin: 0 auto; }
.nn-rebours-cartes-verre__titre { margin: 0 0 24px; font-size: clamp(24px, 5vw, 40px); line-height: 1.15; overflow-wrap: anywhere; }
.nn-rebours-cartes-verre__unites { display: flex; flex-wrap: wrap; justify-content: center; gap: clamp(6px, 2vw, 14px); }
.nn-rebours-cartes-verre__bloc { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: clamp(64px, 18vw, 112px); padding: clamp(10px, 2.5vw, 16px) 6px; border-radius: 14px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.22); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
.nn-rebours-cartes-verre__nombre { font-size: clamp(28px, 8vw, 52px); font-weight: 800; line-height: 1; }
.nn-rebours-cartes-verre__libelle { margin-top: 6px; font-size: clamp(10px, 2.8vw, 13px); letter-spacing: 0.06em; text-transform: uppercase; opacity: 0.8; }
.nn-rebours-cartes-verre__bloc { border-radius: 18px; }
.nn-rebours-cartes-verre__fin { font-size: 18px; font-weight: 600; }
.nn-rebours-cartes-verre__bouton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 44px;
  max-width: 100%;
  padding: 12px 24px;
  border: 0;
  border-radius: 999px;
  background: var(--nn-bouton);
  color: var(--nn-texte-bouton);
  font: inherit;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
@media (hover: hover) { .nn-rebours-cartes-verre__bouton:hover { transform: translateY(-2px); box-shadow: 0 8px 20px color-mix(in srgb, var(--nn-bouton) 35%, transparent); } }
.nn-rebours-cartes-verre__bouton:active { transform: scale(0.97); }
.nn-rebours-cartes-verre__bouton { margin-top: 26px; }
@media (prefers-reduced-motion: reduce) {
  .nn-rebours-cartes-verre, .nn-rebours-cartes-verre *, .nn-rebours-cartes-verre *::before, .nn-rebours-cartes-verre *::after {
    animation: none !important;
    transition: none !important;
  }
}
{% endstylesheet %}

{% javascript %}
if (!customElements.get("nn-rebours-cartes-verre")) {
  customElements.define("nn-rebours-cartes-verre", class extends HTMLElement {
    connectedCallback() {
      if (this.dataset.nnPret) return;
      this.dataset.nnPret = "1";
      var soi = this;
      this.editeur = this.hasAttribute("data-editeur");
      this.zone = this.dataset.zone || "UTC";
      this.unites = {};
      this.dernier = {};
      ["j", "h", "m", "s"].forEach(function (u) { soi.unites[u] = Array.prototype.slice.call(soi.querySelectorAll('[data-unite="' + u + '"]')); });
      this.libelles = String(this.dataset.libelles || "j,h,min,s").split(",").map(function (x) { return x.trim(); });
      this.debut = this.lireDate(this.dataset.debut);
      this.fin = this.calculerFin();
      if (this.querySelector("[data-progres]") && !Number.isFinite(this.debut)) this.fin = NaN;
      this.ecrireEcheance();
      this.brancherCopie();
      this.tic();
    }
    disconnectedCallback() {
      clearTimeout(this.minuterie);
      delete this.dataset.nnPret;
      delete this.dataset.etat;
    }
    // Écart (ms) entre l'heure locale du fuseau et UTC à l'instant t (heure d'été comprise)
    decalage(t) {
      try {
        if (!this.formateur) this.formateur = new Intl.DateTimeFormat("en-US", { timeZone: this.zone, hourCycle: "h23", year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric" });
        var p = {};
        this.formateur.formatToParts(new Date(t)).forEach(function (x) { p[x.type] = x.value; });
        return Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), Number(p.hour) % 24, Number(p.minute), Number(p.second)) - Math.floor(t / 1000) * 1000;
      } catch (e) { return NaN; }
    }
    // Instant UTC d'une heure « murale » du fuseau choisi
    instant(a, mo, j, h, mi, s) {
      var mur = Date.UTC(a, mo - 1, j, h, mi, s);
      return mur - this.decalage(mur - this.decalage(mur));
    }
    // Date saisie par le marchand : 24/12/2026 18:00, 2026-12-24 18:00 (fuseau choisi) ou ISO avec décalage
    lireDate(brut) {
      var texte = String(brut || "").trim();
      if (!texte) return NaN;
      if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/i.test(texte)) return Date.parse(texte);
      var m = /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[\sT]+(\d{1,2})(?:[:hH](\d{2})?)?)?$/.exec(texte);
      var p = m ? [m[1], m[2], m[3], m[4], m[5]] : null;
      if (!p) {
        m = /^(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4})(?:[\s,]+(?:[aA]\s+)?(\d{1,2})(?:[:hH](\d{2})?)?)?$/.exec(texte);
        if (m) p = [m[3], m[2], m[1], m[4], m[5]];
      }
      if (!p) return NaN;
      var sansHeure = p[3] === undefined;
      var n = p.map(function (x) { return Number(x || 0); });
      if (n[1] < 1 || n[1] > 12 || n[2] < 1 || n[2] > 31 || n[3] > 23 || n[4] > 59) return NaN;
      return sansHeure ? this.instant(n[0], n[1], n[2], 23, 59, 59) : this.instant(n[0], n[1], n[2], n[3], n[4], 0);
    }
    lireJour(x) {
      var m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(x);
      var a, mo, j;
      if (m) { a = m[1]; mo = m[2]; j = m[3]; }
      else {
        m = /^(\d{1,2})[\/.](\d{1,2})[\/.](\d{4})$/.exec(x);
        if (!m) return "";
        a = m[3]; mo = m[2]; j = m[1];
      }
      return a + "-" + String(mo).padStart(2, "0") + "-" + String(j).padStart(2, "0");
    }
    calculerFin() {
      var mode = this.dataset.mode || "date";
      if (mode === "quotidien") return this.prochaineLimite();
      var fin = this.lireDate(this.dataset.fin);
      if (mode !== "visiteur") return fin;
      // Offre par visiteur : début mémorisé UNE fois, jamais remis à zéro ; sans mémoire fiable, rien n'est affiché
      var heures = Number(this.dataset.duree);
      if (!(heures > 0)) return NaN;
      var debut = Date.now();
      if (!this.editeur) {
        var cle = "nn-rebours-visiteur:" + (this.dataset.cle || "offre");
        try {
          var lu = Number(localStorage.getItem(cle));
          if (lu > 0 && lu <= Date.now()) debut = lu;
          else localStorage.setItem(cle, String(debut));
          if (Number(localStorage.getItem(cle)) !== debut) return NaN;
        } catch (e) { return NaN; }
      }
      this.debut = debut;
      var finVisiteur = debut + heures * 3600000;
      return Number.isFinite(fin) ? Math.min(fin, finVisiteur) : finVisiteur;
    }
    // Prochaine heure limite quotidienne réelle (jours d'expédition et jours fermés du marchand)
    prochaineLimite() {
      var hm = /^(\d{1,2})(?:[:hH](\d{2})?)?$/.exec(String(this.dataset.heure || "").trim());
      if (!hm || Number(hm[1]) > 23 || Number(hm[2] || 0) > 59) return NaN;
      var maintenant = Date.now();
      var dec = this.decalage(maintenant);
      if (!Number.isFinite(dec)) return NaN;
      var mur = new Date(maintenant + dec);
      var jours = this.dataset.jours || "1-5";
      var soi = this;
      var fermes = String(this.dataset.fermes || "").split(/[\s,;]+/).map(function (x) { return soi.lireJour(x); });
      for (var k = 0; k < 21; k++) {
        var jour = new Date(Date.UTC(mur.getUTCFullYear(), mur.getUTCMonth(), mur.getUTCDate() + k));
        var semaine = jour.getUTCDay();
        if (jours === "1-5" && (semaine === 0 || semaine === 6)) continue;
        if (jours === "1-6" && semaine === 0) continue;
        if (fermes.indexOf(jour.toISOString().slice(0, 10)) >= 0) continue;
        var limite = this.instant(jour.getUTCFullYear(), jour.getUTCMonth() + 1, jour.getUTCDate(), Number(hm[1]), Number(hm[2] || 0), 0);
        if (limite > maintenant) {
          this.ecartJours = Math.round((Date.UTC(jour.getUTCFullYear(), jour.getUTCMonth(), jour.getUTCDate()) - Date.UTC(mur.getUTCFullYear(), mur.getUTCMonth(), mur.getUTCDate())) / 86400000);
          this.jourLimite = jour;
          return limite;
        }
      }
      return NaN;
    }
    ecrireEcheance() {
      if (!Number.isFinite(this.fin)) return;
      var langue = document.documentElement.lang || "fr";
      var texte = "";
      try {
        var o = { weekday: "long", day: "numeric", month: "long", hour: "2-digit", minute: "2-digit", timeZone: this.zone };
        if (new Date(this.fin).getFullYear() !== new Date().getFullYear()) o.year = "numeric";
        texte = new Intl.DateTimeFormat(langue, o).format(new Date(this.fin));
      } catch (e) { texte = new Date(this.fin).toLocaleString(); }
      this.querySelectorAll("[data-echeance]").forEach(function (n) { n.textContent = texte; });
      if (!this.jourLimite) return;
      var nom = this.ecartJours === 0 ? this.dataset.aujourdhui || "" : this.ecartJours === 1 ? this.dataset.demain || "" : "";
      if (!nom) {
        try { nom = new Intl.DateTimeFormat(langue, { weekday: "long", timeZone: "UTC" }).format(this.jourLimite); } catch (e) { nom = ""; }
      }
      this.querySelectorAll("[data-jour]").forEach(function (n) { n.textContent = nom; });
    }
    brancherCopie() {
      var soi = this;
      var bouton = this.querySelector("[data-copier]");
      var code = this.querySelector("[data-code]");
      var statut = this.querySelector("[data-statut]");
      if (!bouton || !code || bouton.dataset.nnBranche) return;
      bouton.dataset.nnBranche = "1";
      var dire = function (texte) {
        if (statut) statut.textContent = texte;
        soi.classList.add("est-copie");
        clearTimeout(soi.minuterieCopie);
        soi.minuterieCopie = setTimeout(function () { soi.classList.remove("est-copie"); if (statut) statut.textContent = ""; }, 2500);
      };
      var selectionner = function () {
        try {
          var plage = document.createRange();
          plage.selectNodeContents(code);
          var sel = window.getSelection();
          sel.removeAllRanges();
          sel.addRange(plage);
          dire(bouton.dataset.selection || "Code sélectionné");
        } catch (e) { dire(""); }
      };
      bouton.addEventListener("click", function () {
        var texte = code.textContent.trim();
        try {
          navigator.clipboard.writeText(texte).then(function () { dire(bouton.dataset.copie || "Copié !"); }, selectionner);
        } catch (e) { selectionner(); }
      });
    }
    tic() {
      clearTimeout(this.minuterie);
      var soi = this;
      var maintenant = Date.now();
      var reste = this.fin - maintenant;
      if (!Number.isFinite(reste)) return this.invalide();
      if (reste <= 0) {
        if (this.dataset.mode === "quotidien") {
          this.fin = this.prochaineLimite();
          this.ecrireEcheance();
          if (!(this.fin > maintenant)) return this.invalide();
          reste = this.fin - maintenant;
        } else {
          var prolongation = (Number(this.dataset.prolongation) || 0) * 60000;
          if (maintenant < this.fin + prolongation) {
            this.afficher({ j: 0, h: 0, m: 0, s: 0 });
            this.etat("direct");
            this.minuterie = setTimeout(function () { soi.tic(); }, Math.min(60000, this.fin + prolongation - maintenant + 50));
            return;
          }
          return this.terminer();
        }
      }
      var s = Math.floor(reste / 1000);
      this.afficher({ j: Math.floor(s / 86400), h: Math.floor((s % 86400) / 3600), m: Math.floor((s % 3600) / 60), s: s % 60 });
      this.etat("pendant");
      this.actif = true;
      this.minuterie = setTimeout(function () { soi.tic(); }, 1000 - (Date.now() % 1000) + 20);
    }
    afficher(v) {
      var soi = this;
      var flip = this.hasAttribute("data-flip");
      var sansJours = v.j === 0;
      this.querySelectorAll("[data-bloc-jours]").forEach(function (n) { n.hidden = sansJours; });
      ["j", "h", "m", "s"].forEach(function (u) {
        var texte = String(v[u]).padStart(2, "0");
        if (soi.dernier[u] === texte) return;
        var premier = soi.dernier[u] === undefined;
        soi.dernier[u] = texte;
        soi.unites[u].forEach(function (n) {
          n.textContent = texte;
          if (flip && !premier) {
            var t = n.closest("[data-tuile]") || n;
            t.classList.remove("est-change");
            void t.offsetWidth;
            t.classList.add("est-change");
          }
        });
      });
      if (!this.joursMax) this.joursMax = Math.max(1, Number(this.dataset.joursMax) || v.j || 1);
      var max = { j: this.joursMax, h: 24, m: 60, s: 60 };
      this.querySelectorAll("[data-cercle]").forEach(function (n) {
        n.style.setProperty("--nn-p", Math.min(1, v[n.getAttribute("data-cercle")] / max[n.getAttribute("data-cercle")]).toFixed(4));
      });
      if (Number.isFinite(this.debut) && this.fin > this.debut) {
        var p = Math.min(1, Math.max(0, (Date.now() - this.debut) / (this.fin - this.debut)));
        this.querySelectorAll("[data-progres]").forEach(function (n) { n.style.setProperty("--nn-p", p.toFixed(4)); });
        this.querySelectorAll("[data-pourcent]").forEach(function (n) { n.textContent = Math.floor(p * 100) + " %"; });
      }
      var textes = this.querySelectorAll("[data-texte]");
      if (!textes.length) return;
      var l = this.libelles;
      var morceaux = [];
      if (this.dataset.format === "horloge") {
        morceaux.push(String(v.h + v.j * 24).padStart(2, "0") + ":" + String(v.m).padStart(2, "0") + ":" + String(v.s).padStart(2, "0"));
      } else {
        if (v.j) morceaux.push(v.j + " " + (l[0] || "j"));
        if (v.j || v.h) morceaux.push(v.h + " " + (l[1] || "h"));
        morceaux.push(v.m + " " + (l[2] || "min"));
        if (!this.hasAttribute("data-sans-secondes")) morceaux.push(v.s + " " + (l[3] || "s"));
      }
      var phrase = morceaux.join(" ");
      textes.forEach(function (n) { if (n.textContent !== phrase) n.textContent = phrase; });
    }
    etat(e) {
      if (this.dataset.etat === e) return;
      var avant = this.dataset.etat;
      this.dataset.etat = e;
      this.querySelectorAll("[data-pendant]").forEach(function (n) { n.hidden = e !== "pendant"; });
      this.querySelectorAll("[data-direct]").forEach(function (n) { n.hidden = e !== "direct"; });
      this.querySelectorAll("[data-apres]").forEach(function (n) { n.hidden = e !== "apres"; });
      // Une seule annonce polie, et seulement si l'offre se termine pendant la visite
      var annonce = this.querySelector("[data-annonce]");
      if (annonce && avant && this.actif && (e === "apres" || e === "direct")) annonce.textContent = annonce.getAttribute(e === "direct" ? "data-texte-direct" : "data-texte-fin") || "";
    }
    montrer(selecteur) {
      this.querySelectorAll(selecteur).forEach(function (n) { n.hidden = false; });
    }
    terminer() {
      this.afficher({ j: 0, h: 0, m: 0, s: 0 });
      this.etat("apres");
      if (this.dataset.aLaFin !== "masquer") return;
      if (this.editeur) this.montrer("[data-note-masque]");
      else this.hidden = true;
    }
    invalide() {
      this.etat("invalide");
      if (this.editeur) this.montrer("[data-alerte]");
      else this.hidden = true;
    }
  });
}
{% endjavascript %}
Télécharger le fichier .liquid

Licence NAHNAH AI : utilisation libre dans tes boutiques (y compris commerciales), modification permise, revente de l’élément seul interdite.

Prompt à copier

À coller dans Claude ou ChatGPT avec le fichier téléchargé (ou le code copié) : l’assistant t’aide à l’installer et à l’adapter à ta marque.

Tu es un expert des thèmes Shopify Online Store 2.0. Aide-moi à installer puis à adapter à ma marque cet élément gratuit de la bibliothèque NAHNAH AI.

Élément : « Cartes effet verre » (Comptes à rebours)
Description : Cartes translucides façon verre dépoli sur un fond de couleurs floues qui dérivent lentement, chiffres nets et bouton. Fond immobile si le visiteur réduit les animations.
Page de l’élément (aperçu en direct) : https://ai.nahnah.shop/bibliotheque-shopify/rebours-cartes-verre
Fichier : nahnah-rebours-cartes-verre.liquid (bouton « Télécharger » de la page) ou le code copié avec le bouton « Copier » : je te le joins ou je le colle après ce message.
Emplacement dans le thème : sections/nahnah-rebours-cartes-verre.liquid (Liquid, puis {% stylesheet %} et {% javascript %}, dans ce seul fichier).

Étapes d’installation :
1. Dans Shopify : Boutique en ligne → Thèmes → bouton « … » du thème → Modifier le code.
2. Dossier « sections » → « Ajouter une section » (ou « Nouveau fichier ») → nomme-la « nahnah-rebours-cartes-verre ».
3. Efface le contenu proposé, colle le code de l’onglet Liquid (fichier complet), puis Enregistrer.
4. Ouvre Personnaliser → Ajouter une section → choisis-la, règle les textes et les couleurs, puis Enregistrer.

Ce que j’attends de toi :
- Guide-moi pas à pas dans Shopify : Boutique en ligne → Thèmes → « … » → Modifier le code → dossier sections → Ajouter une section, puis Personnaliser → Ajouter une section.
- Propose-moi d’adapter les couleurs, les textes et les liens à ma marque : demande d’abord mes couleurs (codes hexadécimaux), mon ton et mes liens.
- Garde le bloc {% schema %} et tous ses réglages (identifiants, types, presets) : je dois pouvoir tout régler ensuite dans l’éditeur de thème.
- Rends-moi le fichier complet modifié, prêt à coller, sans faux avis, fausse urgence ni chiffre inventé.

Si tu as le connecteur NAHNAH AI (MCP), récupère directement le code exact avec l’outil obtenir_code_element (id : rebours-cartes-verre).

Installer dans ma boutique

En un clic, la section est ajoutée à un thème de ta boutique Shopify connectée (un thème non publié par défaut).

Chargement de tes boutiques…

Installer dans Shopify

  1. Dans Shopify : Boutique en ligne → Thèmes → bouton « … » du thème → Modifier le code.
  2. Dossier « sections » → « Ajouter une section » (ou « Nouveau fichier ») → nomme-la « nahnah-rebours-cartes-verre ».
  3. Efface le contenu proposé, colle le code de l’onglet Liquid (fichier complet), puis Enregistrer.
  4. Ouvre Personnaliser → Ajouter une section → choisis-la, règle les textes et les couleurs, puis Enregistrer.

Installer via le MCP

Depuis Claude, Claude Code ou ChatGPT connecté à NAHNAH AI, demande : « Donne-moi le code de l’élément rebours-cartes-verre de la bibliothèque NAHNAH AI et les étapes pour l’installer ». L’assistant récupère le code et les étapes (outil obtenir_code_element), gratuitement.

Boutique connectée à NAHNAH AI : l’assistant peut aussi l’installer directement (outil installer_element).

Connecter le MCP

Dans la même catégorie