Storefront modal + email templating: theme extension, redesign, editable receipt
Storefront (theme app extension recesso-storefront): - App block + app embed che linkano a /apps/recesso; modal via iframe con fallback progressive-enhancement; apertura immediata, dimensione fissa - Redesign form: pattern modal moderno (header/footer fissi, corpo scorrevole), modalita' embed, popover info "i", testo ridotto, de-AI, trattini al posto degli em-dash Ricevuta email: - Dati per-shop dinamici da Shopify (nome, url, link stato ordine) via getShopInfo + statusPageUrl - Template editabili VINCOLATI (oggetto/introduzione/nota) con segnaposto; layout fisso e conforme (dichiarazione, timestamp, avviso art. 54-bis) - Pagina admin (Polaris) con anteprima live + ripristina default - Campi email in Settings (Prisma) + migrazioni Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
This commit is contained in:
106
app/extensions/recesso-storefront/assets/recesso-storefront.css
Normal file
106
app/extensions/recesso-storefront/assets/recesso-storefront.css
Normal file
@@ -0,0 +1,106 @@
|
||||
/* Stile dei blocchi storefront di recesso. Scoped con prefisso .recesso- per non
|
||||
interferire col tema del merchant. */
|
||||
|
||||
/* App block "Recesso - pulsante" */
|
||||
.recesso-block { margin: 12px 0; }
|
||||
.recesso-align-center { text-align: center; }
|
||||
.recesso-align-right { text-align: right; }
|
||||
|
||||
.recesso-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
font-size: 15px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.recesso-button {
|
||||
display: inline-block;
|
||||
padding: 12px 20px;
|
||||
min-height: 44px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
background: #1a1a1a;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
transition: opacity .15s ease;
|
||||
}
|
||||
.recesso-button:hover { opacity: .9; }
|
||||
.recesso-link:focus-visible,
|
||||
.recesso-button:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* App embed "Recesso - link globale" */
|
||||
.recesso-embed { padding: 12px 16px; text-align: center; }
|
||||
.recesso-embed-left { text-align: left; }
|
||||
.recesso-embed-right { text-align: right; }
|
||||
.recesso-embed-link {
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.recesso-embed-link:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Modal (iframe sul flusso /apps/recesso) */
|
||||
.recesso-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2147483000;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
padding: 16px;
|
||||
}
|
||||
.recesso-modal-overlay.is-open { display: flex; }
|
||||
.recesso-modal {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
height: min(760px, 92vh); /* fissa: apertura immediata; header/footer ancorati, solo il corpo scorre */
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 24px 70px rgba(0, 0, 0, .35);
|
||||
}
|
||||
.recesso-modal-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
display: block;
|
||||
}
|
||||
.recesso-modal-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, .06);
|
||||
color: #111111;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.recesso-modal-close:hover { background: rgba(0, 0, 0, .12); }
|
||||
.recesso-modal-close:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.recesso-modal { height: 92vh; max-height: none; border-radius: 12px; }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.recesso-modal { background: #1a1a1a; }
|
||||
.recesso-modal-close { background: rgba(255, 255, 255, .12); color: #ffffff; }
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/* Modal del flusso di recesso.
|
||||
Progressive enhancement: i link puntano a /apps/recesso (funzionano senza JS
|
||||
come pagina piena). Se questo script è caricato, intercetta QUALSIASI link a
|
||||
/apps/recesso - anche una voce di menu o un testo linkato dal merchant - e lo
|
||||
apre in un modal con iframe sul flusso (stesso dominio → framing consentito).
|
||||
Opt-out per singolo link: attributo data-recesso-no-modal. */
|
||||
(function () {
|
||||
if (window.__recessoModalInit) return; // guard: eseguito una volta sola
|
||||
window.__recessoModalInit = true;
|
||||
|
||||
var PROXY_PATH = "/apps/recesso";
|
||||
var overlay = null;
|
||||
var lastFocus = null;
|
||||
|
||||
function build() {
|
||||
var o = document.createElement("div");
|
||||
o.className = "recesso-modal-overlay";
|
||||
o.setAttribute("role", "dialog");
|
||||
o.setAttribute("aria-modal", "true");
|
||||
o.setAttribute("aria-label", "Recesso dal contratto");
|
||||
o.innerHTML =
|
||||
'<div class="recesso-modal">' +
|
||||
'<button type="button" class="recesso-modal-close" aria-label="Chiudi">×</button>' +
|
||||
'<iframe class="recesso-modal-frame" title="Recesso dal contratto"></iframe>' +
|
||||
"</div>";
|
||||
o.addEventListener("click", function (e) {
|
||||
if (e.target === o) close();
|
||||
});
|
||||
o.querySelector(".recesso-modal-close").addEventListener("click", close);
|
||||
return o;
|
||||
}
|
||||
|
||||
function onKey(e) {
|
||||
if (e.key === "Escape" || e.key === "Esc") close();
|
||||
}
|
||||
|
||||
function open(url) {
|
||||
lastFocus = document.activeElement;
|
||||
if (!overlay) {
|
||||
overlay = build();
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
overlay.querySelector(".recesso-modal-frame").src = url;
|
||||
overlay.classList.add("is-open");
|
||||
document.documentElement.style.overflow = "hidden";
|
||||
document.addEventListener("keydown", onKey);
|
||||
var btn = overlay.querySelector(".recesso-modal-close");
|
||||
if (btn && btn.focus) btn.focus();
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!overlay) return;
|
||||
overlay.classList.remove("is-open");
|
||||
overlay.querySelector(".recesso-modal-frame").src = "about:blank";
|
||||
document.documentElement.style.overflow = "";
|
||||
document.removeEventListener("keydown", onKey);
|
||||
if (lastFocus && lastFocus.focus) lastFocus.focus();
|
||||
}
|
||||
|
||||
function normalizePath(p) {
|
||||
return (p || "").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
// Click delegato: intercetta ogni <a> che punta a /apps/recesso (path match),
|
||||
// salvo opt-out esplicito. Funziona per blocchi app, app embed e link/menu
|
||||
// creati dal merchant. Solo click primario, senza modificatori/target.
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
var a = e.target.closest ? e.target.closest("a[href]") : null;
|
||||
if (!a || a.hasAttribute("data-recesso-no-modal")) return;
|
||||
var path;
|
||||
try {
|
||||
path = normalizePath(new URL(a.href, window.location.origin).pathname);
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
if (path !== PROXY_PATH) return;
|
||||
e.preventDefault();
|
||||
open(a.getAttribute("href") || PROXY_PATH);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,56 @@
|
||||
{%- comment -%}
|
||||
App embed "Recesso - link globale". Attivabile site-wide dal theme editor
|
||||
(Impostazioni tema → App embed). Carica CSS+JS su TUTTE le pagine: questo abilita
|
||||
il modal su QUALSIASI link a /apps/recesso (anche voci di menu o testi linkati
|
||||
dal merchant). In più, opzionalmente, mostra un proprio link discreto a fondo pagina.
|
||||
CSS/JS in assets/.
|
||||
{%- endcomment -%}
|
||||
|
||||
{{ 'recesso-storefront.css' | asset_url | stylesheet_tag }}
|
||||
{{ 'recesso-storefront.js' | asset_url | script_tag }}
|
||||
|
||||
{% if block.settings.show_link %}
|
||||
<div class="recesso-embed recesso-embed-{{ block.settings.alignment }}">
|
||||
<a
|
||||
class="recesso-embed-link"
|
||||
href="/apps/recesso"
|
||||
>{{ block.settings.label | default: 'Recedere dal contratto qui' | escape }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% schema %}
|
||||
{
|
||||
"name": "Recesso - link globale",
|
||||
"target": "body",
|
||||
"settings": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": "Attivando questo embed, qualsiasi link a /apps/recesso nel tuo tema (voci di menu, testi linkati, ecc.) si apre in un popup. Puoi anche mostrare un link discreto a fondo pagina."
|
||||
},
|
||||
{
|
||||
"type": "checkbox",
|
||||
"id": "show_link",
|
||||
"label": "Mostra anche un link a fondo pagina",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "label",
|
||||
"label": "Testo del link",
|
||||
"info": "Per legge deve essere inequivocabile.",
|
||||
"default": "Recedere dal contratto qui"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "alignment",
|
||||
"label": "Allineamento",
|
||||
"options": [
|
||||
{ "value": "left", "label": "Sinistra" },
|
||||
{ "value": "center", "label": "Centro" },
|
||||
{ "value": "right", "label": "Destra" }
|
||||
],
|
||||
"default": "center"
|
||||
}
|
||||
]
|
||||
}
|
||||
{% endschema %}
|
||||
@@ -0,0 +1,87 @@
|
||||
{%- comment -%}
|
||||
App block "Recesso - pulsante". Il merchant lo aggiunge in QUALSIASI sezione
|
||||
dal theme editor e lo personalizza. Linka al flusso via App Proxy /apps/recesso.
|
||||
Se "Apri in un popup" è attivo, il JS apre un modal con iframe sul flusso
|
||||
(fallback: pagina piena se JS off). CSS/JS in assets/ (le app block non
|
||||
ammettono i tag stylesheet/javascript inline).
|
||||
{%- endcomment -%}
|
||||
|
||||
{%- assign rc_label = block.settings.label | default: 'Recedere dal contratto qui' -%}
|
||||
|
||||
{{ 'recesso-storefront.css' | asset_url | stylesheet_tag }}
|
||||
{% if block.settings.use_modal %}{{ 'recesso-storefront.js' | asset_url | script_tag }}{% endif %}
|
||||
|
||||
<div class="recesso-block recesso-align-{{ block.settings.alignment }}" {{ block.shopify_attributes }}>
|
||||
<a
|
||||
class="recesso-{{ block.settings.style }}"
|
||||
href="/apps/recesso"
|
||||
{% unless block.settings.use_modal %}data-recesso-no-modal{% endunless %}
|
||||
{% if block.settings.open_new_tab and block.settings.use_modal == false %}target="_blank" rel="noopener"{% endif %}
|
||||
{%- if block.settings.style == 'button' and block.settings.button_bg != blank %} style="background:{{ block.settings.button_bg }};color:{{ block.settings.button_text | default: '#ffffff' }};"{% endif -%}
|
||||
>{{ rc_label | escape }}</a>
|
||||
</div>
|
||||
|
||||
{% schema %}
|
||||
{
|
||||
"name": "Recesso - pulsante",
|
||||
"target": "section",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "label",
|
||||
"label": "Testo del link",
|
||||
"info": "Per legge deve essere inequivocabile (es. \"Recedere dal contratto qui\").",
|
||||
"default": "Recedere dal contratto qui"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "style",
|
||||
"label": "Stile",
|
||||
"options": [
|
||||
{ "value": "link", "label": "Link" },
|
||||
{ "value": "button", "label": "Bottone" }
|
||||
],
|
||||
"default": "button"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"id": "alignment",
|
||||
"label": "Allineamento",
|
||||
"options": [
|
||||
{ "value": "left", "label": "Sinistra" },
|
||||
{ "value": "center", "label": "Centro" },
|
||||
{ "value": "right", "label": "Destra" }
|
||||
],
|
||||
"default": "left"
|
||||
},
|
||||
{
|
||||
"type": "checkbox",
|
||||
"id": "use_modal",
|
||||
"label": "Apri in un popup (modal)",
|
||||
"info": "Se disattivo, apre la pagina intera del flusso.",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"type": "checkbox",
|
||||
"id": "open_new_tab",
|
||||
"label": "Apri in una nuova scheda",
|
||||
"info": "Solo se il popup è disattivo.",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"type": "header",
|
||||
"content": "Colori (solo stile Bottone)"
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"id": "button_bg",
|
||||
"label": "Sfondo bottone"
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"id": "button_text",
|
||||
"label": "Testo bottone"
|
||||
}
|
||||
]
|
||||
}
|
||||
{% endschema %}
|
||||
3
app/extensions/recesso-storefront/shopify.extension.toml
Normal file
3
app/extensions/recesso-storefront/shopify.extension.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
name = "recesso-storefront"
|
||||
type = "theme"
|
||||
uid = "32fd4d4f-a884-a3be-0335-9696fbc24efd646eafe9"
|
||||
Reference in New Issue
Block a user