mkfashion-sdk 2.4.8 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gtm-snippet.js +116 -0
- package/index.html +116 -17
- package/mkfashion-sdk-2.5.0.tgz +0 -0
- package/package.json +2 -2
- package/src/mkfashion.js +252 -10
- package/mkfashion-sdk-2.4.4.tgz +0 -0
- package/mkfashion-sdk-2.4.6.tgz +0 -0
- package/mkfashion-sdk-2.4.7.tgz +0 -0
- package/mkfashion-sdk-2.4.8.tgz +0 -0
- package/test-e2e.html +0 -175
- package/test-responsive.html +0 -45
package/gtm-snippet.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mKFashion SDK — Injection Snippet (versão compacta)
|
|
3
|
+
*
|
|
4
|
+
* Auto-detecta o SKU do produto na PDP, carrega o SDK e renderiza o botão.
|
|
5
|
+
* Quando o cliente adiciona ao carrinho dentro do provador, faz POST
|
|
6
|
+
* pra /cart/add.js com tamanho/cor se disponíveis.
|
|
7
|
+
*
|
|
8
|
+
* USO: cole o conteúdo no GTM (Custom HTML tag), no console DevTools de
|
|
9
|
+
* uma PDP, ou converta pra bookmarklet.
|
|
10
|
+
*
|
|
11
|
+
* CONFIG: troque SEU_PROJECT_ID pelo projectId real do mKFashion.
|
|
12
|
+
*/
|
|
13
|
+
(function () {
|
|
14
|
+
// Pode ser sobrescrito setando window.MKFASHION_PROJECT_ID antes deste script
|
|
15
|
+
// (útil pra GTM Variables ou ambientes de teste).
|
|
16
|
+
var PROJECT_ID = window.MKFASHION_PROJECT_ID || 'SEU_PROJECT_ID';
|
|
17
|
+
|
|
18
|
+
if (window.mkfashion) return go();
|
|
19
|
+
var s = document.createElement('script');
|
|
20
|
+
s.src = 'https://unpkg.com/mkfashion-sdk/src/mkfashion.js';
|
|
21
|
+
s.async = true;
|
|
22
|
+
s.onload = go;
|
|
23
|
+
document.head.appendChild(s);
|
|
24
|
+
|
|
25
|
+
// Detecta SKU do produto na PDP, em ordem de prioridade.
|
|
26
|
+
function detect() {
|
|
27
|
+
// 1. Override manual via ?sku= ou ?identifier=
|
|
28
|
+
var qs = new URLSearchParams(location.search);
|
|
29
|
+
if (qs.get('sku')) return qs.get('sku');
|
|
30
|
+
if (qs.get('identifier')) return qs.get('identifier');
|
|
31
|
+
|
|
32
|
+
// 2. Konfidency (Marisa, Renner e outras marcas BR usam esse serviço de reviews)
|
|
33
|
+
if (window.konfidencyData && window.konfidencyData.sku) {
|
|
34
|
+
return String(window.konfidencyData.sku);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. dataLayer (GA4 / Universal Analytics Enhanced Ecommerce)
|
|
38
|
+
if (window.dataLayer) {
|
|
39
|
+
for (var i = window.dataLayer.length - 1; i >= 0; i--) {
|
|
40
|
+
var x = (window.dataLayer[i] || {}).ecommerce;
|
|
41
|
+
if (!x) continue;
|
|
42
|
+
var p = (x.detail && x.detail.products && x.detail.products[0]) ||
|
|
43
|
+
(x.items && x.items[0]);
|
|
44
|
+
if (p) return String(p.id || p.item_id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4. JSON-LD Schema.org Product
|
|
49
|
+
var lds = document.querySelectorAll('script[type="application/ld+json"]');
|
|
50
|
+
for (var j = 0; j < lds.length; j++) {
|
|
51
|
+
try {
|
|
52
|
+
var items = [].concat(JSON.parse(lds[j].textContent));
|
|
53
|
+
for (var k = 0; k < items.length; k++) {
|
|
54
|
+
var pr = items[k]['@type'] === 'Product' ? items[k] :
|
|
55
|
+
(items[k]['@graph'] || []).find(function (g) { return g['@type'] === 'Product'; });
|
|
56
|
+
if (pr && (pr.sku || pr.productID)) return String(pr.sku || pr.productID);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function go() {
|
|
65
|
+
var sku = detect();
|
|
66
|
+
if (!sku) {
|
|
67
|
+
console.error('[mKFashion] SKU não detectado. Tente passar ?sku=SEU_SKU na URL.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log('[mKFashion] SKU:', sku);
|
|
71
|
+
|
|
72
|
+
// Cria container se ainda não existe (overlay fixo no canto como fallback)
|
|
73
|
+
if (!document.getElementById('mkfashion-container')) {
|
|
74
|
+
var c = document.createElement('div');
|
|
75
|
+
c.id = 'mkfashion-container';
|
|
76
|
+
c.style.cssText = 'position:fixed;top:90px;right:20px;z-index:99999;background:#fff;padding:8px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.12)';
|
|
77
|
+
document.body.appendChild(c);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Adiciona ao carrinho da plataforma com tamanho/cor se disponíveis
|
|
81
|
+
mkfashion.addToCart(function (p) {
|
|
82
|
+
console.log('[mKFashion] addToCart:', p);
|
|
83
|
+
var item = { id: p.selectedIdentifier, quantity: 1 };
|
|
84
|
+
var props = {};
|
|
85
|
+
if (p.selectedSize) props['Tamanho'] = p.selectedSize;
|
|
86
|
+
if (p.selectedColor) props['Cor'] = p.selectedColor;
|
|
87
|
+
if (Object.keys(props).length) item.properties = props;
|
|
88
|
+
|
|
89
|
+
// Endpoint Shopify-style. Para VTEX/Wake/Magento, ajuste a URL e o body.
|
|
90
|
+
fetch('/cart/add.js', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ items: [item] })
|
|
94
|
+
})
|
|
95
|
+
.then(function (r) { return r.json(); })
|
|
96
|
+
.then(function (d) { console.log('[mKFashion] no carrinho:', d); })
|
|
97
|
+
.catch(function (e) { console.error('[mKFashion] erro cart:', e); });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
mkfashion.init({
|
|
101
|
+
projectId: PROJECT_ID,
|
|
102
|
+
identifier: sku,
|
|
103
|
+
target: '#mkfashion-container'
|
|
104
|
+
}).then(function (r) {
|
|
105
|
+
// Workaround: depois do init() já validar availability, evita re-fetch no
|
|
106
|
+
// click handler. Sem isso, em sites com monitor SPA (New Relic, etc.) o
|
|
107
|
+
// click intercepta o documento e quebra o fetch com "global scope shutting
|
|
108
|
+
// down". Redundante na SDK 2.5.x+ (cache nativo) — mas inofensivo.
|
|
109
|
+
if (r && r.ok) {
|
|
110
|
+
mkfashion.getAvailability = function () {
|
|
111
|
+
return Promise.resolve({ available: true });
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
})();
|
package/index.html
CHANGED
|
@@ -15,35 +15,134 @@
|
|
|
15
15
|
<script>
|
|
16
16
|
(function () {
|
|
17
17
|
var projectid = '69f9e5b0f4eb0c4403319830';
|
|
18
|
-
var identifier = '1881';
|
|
19
18
|
|
|
19
|
+
// ============================================================
|
|
20
|
+
// Detecta o identifier (SKU) da PDP automaticamente.
|
|
21
|
+
// Estratégias em ordem de prioridade — retorna a primeira que casar.
|
|
22
|
+
// ============================================================
|
|
23
|
+
function detectIdentifier() {
|
|
24
|
+
// 1. Query string (?sku=... ou ?identifier=...) — útil pra teste / override
|
|
25
|
+
var qs = new URLSearchParams(location.search);
|
|
26
|
+
var fromUrl = qs.get('sku') || qs.get('identifier');
|
|
27
|
+
if (fromUrl) return { value: fromUrl, source: 'url-param' };
|
|
28
|
+
|
|
29
|
+
// 2. JSON-LD (Schema.org Product) — presente em PDPs com SEO decente
|
|
30
|
+
var scripts = document.querySelectorAll('script[type="application/ld+json"]');
|
|
31
|
+
for (var i = 0; i < scripts.length; i++) {
|
|
32
|
+
try {
|
|
33
|
+
var data = JSON.parse(scripts[i].textContent);
|
|
34
|
+
var items = Array.isArray(data) ? data : [data];
|
|
35
|
+
for (var j = 0; j < items.length; j++) {
|
|
36
|
+
var node = items[j];
|
|
37
|
+
var product = node['@type'] === 'Product' ? node :
|
|
38
|
+
(node['@graph'] && node['@graph'].find(function (g) { return g['@type'] === 'Product'; }));
|
|
39
|
+
if (product) {
|
|
40
|
+
var sku = product.sku || product.mpn || product.productID;
|
|
41
|
+
if (sku) return { value: String(sku), source: 'json-ld' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (e) { /* json inválido — ignora */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Meta tags
|
|
48
|
+
var meta = document.querySelector(
|
|
49
|
+
'meta[itemprop="sku"], meta[property="product:retailer_item_id"], meta[name="sku"]'
|
|
50
|
+
);
|
|
51
|
+
if (meta && meta.content) return { value: meta.content, source: 'meta' };
|
|
52
|
+
|
|
53
|
+
// 4. Microdata em qualquer elemento
|
|
54
|
+
var micro = document.querySelector('[itemprop="sku"]');
|
|
55
|
+
if (micro) {
|
|
56
|
+
var v = micro.getAttribute('content') || (micro.textContent || '').trim();
|
|
57
|
+
if (v) return { value: v, source: 'microdata' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 5. dataLayer (GA4 / Enhanced Ecommerce)
|
|
61
|
+
if (window.dataLayer && window.dataLayer.length) {
|
|
62
|
+
for (var k = window.dataLayer.length - 1; k >= 0; k--) {
|
|
63
|
+
var e = window.dataLayer[k] || {};
|
|
64
|
+
// GA4
|
|
65
|
+
if (e.ecommerce && e.ecommerce.items && e.ecommerce.items[0]) {
|
|
66
|
+
var ga4 = e.ecommerce.items[0].item_id || e.ecommerce.items[0].id;
|
|
67
|
+
if (ga4) return { value: String(ga4), source: 'dataLayer/ga4' };
|
|
68
|
+
}
|
|
69
|
+
// Universal Analytics Enhanced Ecommerce
|
|
70
|
+
if (e.ecommerce && e.ecommerce.detail && e.ecommerce.detail.products && e.ecommerce.detail.products[0]) {
|
|
71
|
+
var ee = e.ecommerce.detail.products[0].id;
|
|
72
|
+
if (ee) return { value: String(ee), source: 'dataLayer/ee' };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 6. Globais de plataforma
|
|
78
|
+
if (window.ShopifyAnalytics && window.ShopifyAnalytics.meta && window.ShopifyAnalytics.meta.product) {
|
|
79
|
+
return { value: String(window.ShopifyAnalytics.meta.product.id), source: 'shopify' };
|
|
80
|
+
}
|
|
81
|
+
// VTEX — várias formas de expor a SKU
|
|
82
|
+
if (window.vtxctx && window.vtxctx.skus && window.vtxctx.skus[0]) {
|
|
83
|
+
return { value: String(window.vtxctx.skus[0]), source: 'vtex/vtxctx' };
|
|
84
|
+
}
|
|
85
|
+
if (window.skuJson_0 && window.skuJson_0.skus && window.skuJson_0.skus[0]) {
|
|
86
|
+
return { value: String(window.skuJson_0.skus[0].sku), source: 'vtex/skuJson' };
|
|
87
|
+
}
|
|
88
|
+
// Konfidency (serviço de reviews usado por várias marcas BR — Marisa, etc.)
|
|
89
|
+
var kd = window.konfidencyData;
|
|
90
|
+
if (kd && kd.product && kd.product.variants && kd.product.variants[0]) {
|
|
91
|
+
return { value: String(kd.product.variants[0]), source: 'konfidency/variant' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Nota: NÃO usamos padrão de URL como fallback (ex: /p/{id} na VTEX
|
|
95
|
+
// retorna o productId, não o SKU). É melhor falhar do que dar dado
|
|
96
|
+
// errado — quem precisar de override deve passar ?sku= na URL.
|
|
97
|
+
|
|
98
|
+
// 7. Fallback hardcoded — útil pra rodar index.html localmente sem
|
|
99
|
+
// estrutura de PDP. Em produção, remova ou ajuste pro seu cenário.
|
|
100
|
+
return { value: '1881', source: 'fallback-hardcoded' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================
|
|
104
|
+
// Inicialização
|
|
105
|
+
// ============================================================
|
|
20
106
|
function initMkFashion() {
|
|
107
|
+
var detected = detectIdentifier();
|
|
108
|
+
console.log('[mKFashion] SKU detectado:', detected.value, '(via ' + detected.source + ')');
|
|
109
|
+
|
|
110
|
+
// Callback de carrinho — recebe o produto escolhido no provador
|
|
111
|
+
// e adiciona ao carrinho da plataforma (exemplo: Shopify).
|
|
21
112
|
mkfashion.addToCart(function (payload) {
|
|
22
|
-
console.log('
|
|
113
|
+
console.log('[mKFashion] addToCart payload:', payload);
|
|
23
114
|
|
|
24
115
|
var variantSku = payload.selectedIdentifier;
|
|
116
|
+
if (!variantSku) {
|
|
117
|
+
console.warn('[mKFashion] produto sem variantSku — pulando adição ao carrinho');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Properties opcionais — passa tamanho/cor se vieram no payload.
|
|
122
|
+
// No Shopify aparecem no carrinho como propriedades de line item.
|
|
123
|
+
var properties = {};
|
|
124
|
+
if (payload.selectedSize) properties['Tamanho'] = payload.selectedSize;
|
|
125
|
+
if (payload.selectedColor) properties['Cor'] = payload.selectedColor;
|
|
126
|
+
|
|
127
|
+
var item = { id: variantSku, quantity: 1 };
|
|
128
|
+
if (Object.keys(properties).length) item.properties = properties;
|
|
25
129
|
|
|
130
|
+
// Endpoint Shopify-style. Ajuste pra sua plataforma:
|
|
131
|
+
// VTEX: POST /api/checkout/pub/orderForm/{id}/items
|
|
132
|
+
// Wake: API própria
|
|
133
|
+
// Magento: rest/V1/carts/mine/items
|
|
26
134
|
fetch('/cart/add.js', {
|
|
27
135
|
method: 'POST',
|
|
28
|
-
headers: {
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
body: JSON.stringify({
|
|
32
|
-
items: [{
|
|
33
|
-
quantity: 1,
|
|
34
|
-
id: variantSku
|
|
35
|
-
}]
|
|
36
|
-
})
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify({ items: [item] })
|
|
37
138
|
})
|
|
38
|
-
.then(function (response) {
|
|
39
|
-
return response.json();
|
|
40
|
-
})
|
|
139
|
+
.then(function (response) { return response.json(); })
|
|
41
140
|
.then(function (data) {
|
|
42
|
-
console.log('
|
|
141
|
+
console.log('[mKFashion] produto adicionado ao carrinho:', data);
|
|
43
142
|
// document.dispatchEvent(new CustomEvent('cart:updated'));
|
|
44
143
|
})
|
|
45
144
|
.catch(function (error) {
|
|
46
|
-
console.error('
|
|
145
|
+
console.error('[mKFashion] erro ao adicionar ao carrinho:', error);
|
|
47
146
|
});
|
|
48
147
|
});
|
|
49
148
|
|
|
@@ -53,7 +152,7 @@
|
|
|
53
152
|
|
|
54
153
|
mkfashion.init({
|
|
55
154
|
projectId: projectid,
|
|
56
|
-
identifier:
|
|
155
|
+
identifier: detected.value,
|
|
57
156
|
target: '#mkfashion-container'
|
|
58
157
|
});
|
|
59
158
|
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mkfashion-sdk",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce",
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce e tracking analytics",
|
|
5
5
|
"main": "src/mkfashion.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Abra test.html no navegador\""
|
package/src/mkfashion.js
CHANGED
|
@@ -19,11 +19,15 @@ const mkfashion = {
|
|
|
19
19
|
|
|
20
20
|
appUrl: 'https://mkfashion.mk3dlabs.com/visualizer',
|
|
21
21
|
apiUrl: 'https://mkfashion-new-api.mk3dlabs.com',
|
|
22
|
+
// BETA: collector apontando para tunnel temporario (Cloudflare quick tunnel).
|
|
23
|
+
// Trocar para 'https://collector.metakosmos.com.br' quando o deploy formal subir.
|
|
24
|
+
collectorUrl: 'https://something-perl-parker-radios.trycloudflare.com',
|
|
22
25
|
debug: false,
|
|
23
26
|
|
|
24
27
|
// DEV - Descomentar para desenvolvimento local
|
|
25
28
|
//appUrl: 'http://localhost:5174/visualizer',
|
|
26
29
|
//apiUrl: 'http://localhost:3007',
|
|
30
|
+
//collectorUrl: 'http://localhost:3030',
|
|
27
31
|
|
|
28
32
|
// ============ ESTADO INTERNO ============
|
|
29
33
|
|
|
@@ -80,17 +84,25 @@ const mkfashion = {
|
|
|
80
84
|
return
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
// Pula re-validação se init() já validou esse projeto+SKU (caso comum:
|
|
88
|
+
// botão renderizado pelo init e clicado depois). Evita fetch redundante
|
|
89
|
+
// no click handler — agente de monitoramento SPA (New Relic etc.) pode
|
|
90
|
+
// interceptar o click e quebrar fetches durante interceptação.
|
|
91
|
+
var availabilityCacheKey = projectId + ':' + identifier
|
|
92
|
+
if (this._availabilityCheckedFor !== availabilityCacheKey) {
|
|
93
|
+
try {
|
|
94
|
+
const availability = await this.getAvailability(projectId, identifier)
|
|
95
|
+
if (!availability.available) {
|
|
96
|
+
console.error('[mKFashion] Produto nao disponivel para try-on:', availability.message)
|
|
97
|
+
alert('Este produto não está disponível para prova virtual.')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('[mKFashion] Erro ao verificar disponibilidade:', error)
|
|
102
|
+
alert('Produto não encontrado ou indisponível.')
|
|
88
103
|
return
|
|
89
104
|
}
|
|
90
|
-
|
|
91
|
-
console.error('[mKFashion] Erro ao verificar disponibilidade:', error)
|
|
92
|
-
alert('Produto não encontrado ou indisponível.')
|
|
93
|
-
return
|
|
105
|
+
this._availabilityCheckedFor = availabilityCacheKey
|
|
94
106
|
}
|
|
95
107
|
|
|
96
108
|
this._config = {
|
|
@@ -119,6 +131,10 @@ const mkfashion = {
|
|
|
119
131
|
|
|
120
132
|
this._isOpen = true
|
|
121
133
|
this._log('Aberto', this._config)
|
|
134
|
+
|
|
135
|
+
// Emite modal_opened — não passa pelo _triggerCallback porque é um evento
|
|
136
|
+
// só de tracking (não tem callback público equivalente).
|
|
137
|
+
try { this._emitToCollector('modal_opened', { projectId, identifier }) } catch (_) {}
|
|
122
138
|
},
|
|
123
139
|
|
|
124
140
|
/** Fecha o modal do provador virtual (esconde sem destruir a sessao) */
|
|
@@ -133,13 +149,21 @@ const mkfashion = {
|
|
|
133
149
|
this._isOpen = false
|
|
134
150
|
this._confirmingExit = false
|
|
135
151
|
this._log('Fechado')
|
|
152
|
+
|
|
153
|
+
// Emite modal_closed (complementa o onClose que já passou por _triggerCallback).
|
|
154
|
+
try {
|
|
155
|
+
this._emitToCollector('modal_closed', {
|
|
156
|
+
projectId: this._config && this._config.projectId,
|
|
157
|
+
identifier: this._config && this._config.identifier
|
|
158
|
+
})
|
|
159
|
+
} catch (_) {}
|
|
136
160
|
},
|
|
137
161
|
|
|
138
162
|
/**
|
|
139
163
|
* Registra callback de adicionar ao carrinho.
|
|
140
164
|
*
|
|
141
165
|
* Payload simplificado (não-Gregory):
|
|
142
|
-
* { mainIdentifier, selectedIdentifier, name, price, sizePrice, selectedSize, productUrl, tryonImageUrl }
|
|
166
|
+
* { mainIdentifier, selectedIdentifier, name, price, sizePrice, selectedSize, selectedColor, productUrl, tryonImageUrl }
|
|
143
167
|
*
|
|
144
168
|
* Projetos Gregory mantêm estrutura legada completa.
|
|
145
169
|
*/
|
|
@@ -279,10 +303,32 @@ const mkfashion = {
|
|
|
279
303
|
|
|
280
304
|
const data = await response.json()
|
|
281
305
|
this._log('Disponibilidade recebida', data)
|
|
306
|
+
|
|
307
|
+
// Tracking: cada checagem de availability é um sinal de "impressão"
|
|
308
|
+
// do produto na vitrine. Funciona mesmo se o usuário não clicar.
|
|
309
|
+
try {
|
|
310
|
+
if (!this._config) this._config = { projectId: resolved, identifier }
|
|
311
|
+
this._emitToCollector('availability_checked', {
|
|
312
|
+
projectId: resolved,
|
|
313
|
+
identifier,
|
|
314
|
+
available: data.available === true,
|
|
315
|
+
reason: data.reason || data.message || null
|
|
316
|
+
})
|
|
317
|
+
} catch (_) {}
|
|
318
|
+
|
|
282
319
|
return data
|
|
283
320
|
|
|
284
321
|
} catch (error) {
|
|
285
322
|
console.error('[mKFashion] Erro ao verificar disponibilidade:', error)
|
|
323
|
+
// Tracking: também emite erros de availability — útil pra detectar 404/500 em prod.
|
|
324
|
+
try {
|
|
325
|
+
if (!this._config) this._config = { projectId: resolved, identifier }
|
|
326
|
+
this._emitToCollector('availability_error', {
|
|
327
|
+
projectId: resolved,
|
|
328
|
+
identifier,
|
|
329
|
+
error: String(error.message || error).slice(0, 200)
|
|
330
|
+
})
|
|
331
|
+
} catch (_) {}
|
|
286
332
|
throw error
|
|
287
333
|
}
|
|
288
334
|
},
|
|
@@ -420,6 +466,9 @@ const mkfashion = {
|
|
|
420
466
|
},
|
|
421
467
|
|
|
422
468
|
_triggerCallback(name, data = null) {
|
|
469
|
+
// Emite pro collector ANTES do callback do cliente (não bloqueia, fire-and-forget).
|
|
470
|
+
try { this._emitToCollector(name, data) } catch (_) {}
|
|
471
|
+
|
|
423
472
|
if (this._callbacks && typeof this._callbacks[name] === 'function') {
|
|
424
473
|
try {
|
|
425
474
|
this._callbacks[name](data)
|
|
@@ -430,6 +479,184 @@ const mkfashion = {
|
|
|
430
479
|
}
|
|
431
480
|
},
|
|
432
481
|
|
|
482
|
+
// ============ TRACKING (mk-collector-api) ============
|
|
483
|
+
|
|
484
|
+
_visitorId: null,
|
|
485
|
+
_sessionId: null,
|
|
486
|
+
_siteContext: null,
|
|
487
|
+
|
|
488
|
+
_getVisitorId() {
|
|
489
|
+
if (this._visitorId) return this._visitorId
|
|
490
|
+
try {
|
|
491
|
+
let v = localStorage.getItem('_mk_sdk_vid')
|
|
492
|
+
if (!v) {
|
|
493
|
+
v = 'v_' + (typeof crypto !== 'undefined' && crypto.randomUUID
|
|
494
|
+
? crypto.randomUUID()
|
|
495
|
+
: Math.random().toString(36).slice(2) + Date.now().toString(36))
|
|
496
|
+
localStorage.setItem('_mk_sdk_vid', v)
|
|
497
|
+
}
|
|
498
|
+
this._visitorId = v
|
|
499
|
+
return v
|
|
500
|
+
} catch (_) {
|
|
501
|
+
this._visitorId = 'v_' + Math.random().toString(36).slice(2)
|
|
502
|
+
return this._visitorId
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
_getSessionId() {
|
|
507
|
+
if (this._sessionId) return this._sessionId
|
|
508
|
+
try {
|
|
509
|
+
let s = sessionStorage.getItem('_mk_sdk_sid')
|
|
510
|
+
if (!s) {
|
|
511
|
+
s = 's_' + (typeof crypto !== 'undefined' && crypto.randomUUID
|
|
512
|
+
? crypto.randomUUID()
|
|
513
|
+
: Math.random().toString(36).slice(2) + Date.now().toString(36))
|
|
514
|
+
sessionStorage.setItem('_mk_sdk_sid', s)
|
|
515
|
+
}
|
|
516
|
+
this._sessionId = s
|
|
517
|
+
return s
|
|
518
|
+
} catch (_) {
|
|
519
|
+
this._sessionId = 's_' + Math.random().toString(36).slice(2)
|
|
520
|
+
return this._sessionId
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
_parseUtm() {
|
|
525
|
+
try {
|
|
526
|
+
const p = new URLSearchParams(location.search)
|
|
527
|
+
const out = {}
|
|
528
|
+
const keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
|
|
529
|
+
for (const k of keys) {
|
|
530
|
+
const v = p.get(k)
|
|
531
|
+
if (v) out[k.replace('utm_', '')] = v
|
|
532
|
+
}
|
|
533
|
+
return Object.keys(out).length > 0 ? out : null
|
|
534
|
+
} catch (_) { return null }
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
_detectPlatform() {
|
|
538
|
+
try {
|
|
539
|
+
if (window.Shopify || window.ShopifyAnalytics) return 'shopify'
|
|
540
|
+
if (window.vtex || window.vtxctx) return 'vtex'
|
|
541
|
+
if (window.wakeCommerce || window.__WAKE__) return 'wake'
|
|
542
|
+
if (window.Konfidency || window.konfidency) return 'konfidency'
|
|
543
|
+
if (window.wc_add_to_cart_params || window.woocommerce_params) return 'woocommerce'
|
|
544
|
+
if (window.dataLayer && Array.isArray(window.dataLayer) && window.dataLayer[0]) {
|
|
545
|
+
const p = window.dataLayer[0].platform
|
|
546
|
+
if (p) return String(p).toLowerCase()
|
|
547
|
+
}
|
|
548
|
+
} catch (_) {}
|
|
549
|
+
return null
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
_detectCurrentProduct() {
|
|
553
|
+
try {
|
|
554
|
+
// 1. dataLayer (GA4/UA padrão)
|
|
555
|
+
if (window.dataLayer && Array.isArray(window.dataLayer)) {
|
|
556
|
+
for (let i = window.dataLayer.length - 1; i >= 0; i--) {
|
|
557
|
+
const item = window.dataLayer[i]
|
|
558
|
+
if (!item) continue
|
|
559
|
+
if (item.product) {
|
|
560
|
+
return {
|
|
561
|
+
sku: item.product.sku || item.product.id || null,
|
|
562
|
+
name: item.product.name || null,
|
|
563
|
+
price: item.product.price || null
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (item.ecommerce && item.ecommerce.items && item.ecommerce.items[0]) {
|
|
567
|
+
const p = item.ecommerce.items[0]
|
|
568
|
+
return { sku: p.item_id || p.id, name: p.item_name || p.name, price: p.price }
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// 2. JSON-LD schema.org/Product
|
|
573
|
+
const lds = document.querySelectorAll('script[type="application/ld+json"]')
|
|
574
|
+
for (const ld of lds) {
|
|
575
|
+
try {
|
|
576
|
+
const j = JSON.parse(ld.textContent || '{}')
|
|
577
|
+
const arr = Array.isArray(j) ? j : [j]
|
|
578
|
+
for (const item of arr) {
|
|
579
|
+
if (item && (item['@type'] === 'Product' || (Array.isArray(item['@type']) && item['@type'].includes('Product')))) {
|
|
580
|
+
return {
|
|
581
|
+
sku: item.sku || item.mpn || item.gtin || null,
|
|
582
|
+
name: item.name || null,
|
|
583
|
+
price: (item.offers && (item.offers.price || (item.offers[0] && item.offers[0].price))) || null
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} catch (_) {}
|
|
588
|
+
}
|
|
589
|
+
// 3. Open Graph (og:type=product)
|
|
590
|
+
const og = (n) => {
|
|
591
|
+
const m = document.querySelector('meta[property="og:' + n + '"]')
|
|
592
|
+
return m && m.content || null
|
|
593
|
+
}
|
|
594
|
+
if (og('type') === 'product') {
|
|
595
|
+
return { sku: null, name: og('title'), price: og('price:amount') }
|
|
596
|
+
}
|
|
597
|
+
} catch (_) {}
|
|
598
|
+
return null
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
_detectSiteContext() {
|
|
602
|
+
if (this._siteContext) return this._siteContext
|
|
603
|
+
try {
|
|
604
|
+
this._siteContext = {
|
|
605
|
+
url: location.href,
|
|
606
|
+
pathname: location.pathname,
|
|
607
|
+
referrer: document.referrer || null,
|
|
608
|
+
title: document.title || null,
|
|
609
|
+
utm: this._parseUtm(),
|
|
610
|
+
userAgent: navigator.userAgent,
|
|
611
|
+
language: navigator.language,
|
|
612
|
+
viewport: window.innerWidth + 'x' + window.innerHeight,
|
|
613
|
+
devicePixelRatio: window.devicePixelRatio || 1,
|
|
614
|
+
platform: this._detectPlatform(),
|
|
615
|
+
detectedProduct: this._detectCurrentProduct()
|
|
616
|
+
}
|
|
617
|
+
} catch (_) {
|
|
618
|
+
this._siteContext = {}
|
|
619
|
+
}
|
|
620
|
+
return this._siteContext
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Envia evento pro mk-collector-api. Fire-and-forget — nunca atrapalha UX.
|
|
625
|
+
* Identificação: X-MK-Project-Id (o próprio projectId do mkfashion).
|
|
626
|
+
*/
|
|
627
|
+
_emitToCollector(eventName, data) {
|
|
628
|
+
try {
|
|
629
|
+
if (!this._config || !this._config.projectId) return
|
|
630
|
+
if (!this.collectorUrl) return
|
|
631
|
+
|
|
632
|
+
const params = { identifier: this._config.identifier || null }
|
|
633
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
634
|
+
Object.assign(params, data)
|
|
635
|
+
} else if (data !== null && data !== undefined) {
|
|
636
|
+
params.value = data
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const payload = JSON.stringify({
|
|
640
|
+
visitorId: this._getVisitorId(),
|
|
641
|
+
sessionId: this._getSessionId(),
|
|
642
|
+
events: [{ name: 'sdk_' + eventName, ts: Date.now(), params: params }],
|
|
643
|
+
site: this._detectSiteContext()
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
fetch(this.collectorUrl + '/v1/track/sdk', {
|
|
647
|
+
method: 'POST',
|
|
648
|
+
keepalive: true,
|
|
649
|
+
headers: {
|
|
650
|
+
'Content-Type': 'application/json',
|
|
651
|
+
'X-MK-Project-Id': this._config.projectId
|
|
652
|
+
},
|
|
653
|
+
body: payload
|
|
654
|
+
}).catch(() => {})
|
|
655
|
+
} catch (e) {
|
|
656
|
+
if (this.debug) console.warn('[mKFashion tracking]', e.message)
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
|
|
433
660
|
_buildUrl() {
|
|
434
661
|
const encoded = encodeURIComponent(this._config.identifier).replace(/\./g, '%2E')
|
|
435
662
|
const url = `${this.appUrl}/${this._config.projectId}/${encoded}`
|
|
@@ -729,6 +956,7 @@ const mkfashion = {
|
|
|
729
956
|
price: product.price || null,
|
|
730
957
|
sizePrice: product.sizePrice || null,
|
|
731
958
|
selectedSize: data?.size || null,
|
|
959
|
+
selectedColor: data?.color || product.color || null,
|
|
732
960
|
produtoVarianteId: product.produtoVarianteId || null,
|
|
733
961
|
productUrl: product.productUrl || null,
|
|
734
962
|
tryonImageUrl: product.tryonImageUrl || null
|
|
@@ -850,9 +1078,17 @@ const mkfashion = {
|
|
|
850
1078
|
|
|
851
1079
|
if (!available) {
|
|
852
1080
|
this._log('Produto indisponível, botão não criado', { projectId, identifier })
|
|
1081
|
+
try {
|
|
1082
|
+
if (!this._config) this._config = { projectId, identifier }
|
|
1083
|
+
this._emitToCollector('button_unavailable', { projectId, identifier })
|
|
1084
|
+
} catch (_) {}
|
|
853
1085
|
return fail('unavailable')
|
|
854
1086
|
}
|
|
855
1087
|
|
|
1088
|
+
// Marca como validado pra open() pular o fetch redundante no click handler.
|
|
1089
|
+
// (Importante em sites com monitores SPA tipo New Relic que interceptam clicks.)
|
|
1090
|
+
this._availabilityCheckedFor = projectId + ':' + identifier
|
|
1091
|
+
|
|
856
1092
|
const isMobile = window.matchMedia('(max-width: 767px)').matches
|
|
857
1093
|
const config = this._normalizeButtonConfig(template, isMobile)
|
|
858
1094
|
const renderer = this._BUTTON_RENDERERS[config.style] || this._BUTTON_RENDERERS['gregory-black']
|
|
@@ -862,6 +1098,12 @@ const mkfashion = {
|
|
|
862
1098
|
|
|
863
1099
|
container.appendChild(element)
|
|
864
1100
|
this._log('Botão criado', { target, projectId, identifier, style: config.style })
|
|
1101
|
+
try {
|
|
1102
|
+
if (!this._config) this._config = { projectId, identifier }
|
|
1103
|
+
this._emitToCollector('button_rendered', {
|
|
1104
|
+
projectId, identifier, style: config.style
|
|
1105
|
+
})
|
|
1106
|
+
} catch (_) {}
|
|
865
1107
|
return { ok: true, reason: 'rendered', element }
|
|
866
1108
|
},
|
|
867
1109
|
|
package/mkfashion-sdk-2.4.4.tgz
DELETED
|
Binary file
|
package/mkfashion-sdk-2.4.6.tgz
DELETED
|
Binary file
|
package/mkfashion-sdk-2.4.7.tgz
DELETED
|
Binary file
|
package/mkfashion-sdk-2.4.8.tgz
DELETED
|
Binary file
|
package/test-e2e.html
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="pt-BR">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<title>mKFashion SDK — E2E Test</title>
|
|
6
|
-
<script src="./src/mkfashion.js"></script>
|
|
7
|
-
<style>
|
|
8
|
-
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 24px; max-width: 900px; }
|
|
9
|
-
.test { margin-bottom: 16px; padding: 12px; background: #f5f5f5; border-radius: 6px; border-left: 4px solid #999; }
|
|
10
|
-
.test.pass { border-color: #16a34a; background: #ecfdf5; }
|
|
11
|
-
.test.fail { border-color: #dc2626; background: #fef2f2; }
|
|
12
|
-
.test h3 { margin: 0 0 6px; font-size: 14px; }
|
|
13
|
-
.test pre { margin: 0; font-size: 11px; white-space: pre-wrap; }
|
|
14
|
-
.target { display: inline-block; margin: 0 8px; padding: 4px 8px; background: white; border: 1px dashed #ccc; }
|
|
15
|
-
</style>
|
|
16
|
-
</head>
|
|
17
|
-
<body>
|
|
18
|
-
<h2>mKFashion SDK — E2E Tests</h2>
|
|
19
|
-
|
|
20
|
-
<div>
|
|
21
|
-
<span>Container 1 (válido):</span><span class="target" id="target-valid"></span>
|
|
22
|
-
<span>Container 2 (válido):</span><span class="target" id="target-2"></span>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<div id="output" style="margin-top: 24px;"></div>
|
|
26
|
-
|
|
27
|
-
<script>
|
|
28
|
-
const projectId = '69c56d5373ecdf64df48e330'
|
|
29
|
-
const validSku = '000756397001'
|
|
30
|
-
const invalidSku = '99999999999999'
|
|
31
|
-
const output = document.getElementById('output')
|
|
32
|
-
|
|
33
|
-
function log(name, passed, details) {
|
|
34
|
-
const div = document.createElement('div')
|
|
35
|
-
div.className = 'test ' + (passed ? 'pass' : 'fail')
|
|
36
|
-
div.innerHTML = `<h3>${passed ? 'PASS' : 'FAIL'} — ${name}</h3><pre>${details}</pre>`
|
|
37
|
-
output.appendChild(div)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function fmt(obj) {
|
|
41
|
-
return JSON.stringify(obj, (k, v) => v instanceof HTMLElement ? `<${v.tagName.toLowerCase()}>` : v, 2)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function run() {
|
|
45
|
-
// ---- TEST 1: init com params completos e SKU válido ----
|
|
46
|
-
const r1 = await mkfashion.init({
|
|
47
|
-
projectId,
|
|
48
|
-
identifier: validSku,
|
|
49
|
-
target: '#target-valid'
|
|
50
|
-
})
|
|
51
|
-
log(
|
|
52
|
-
'init() com SKU válido → ok: true, reason: rendered',
|
|
53
|
-
r1.ok === true && r1.reason === 'rendered' && r1.element instanceof HTMLElement,
|
|
54
|
-
fmt(r1)
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
// ---- TEST 2: init sem target ----
|
|
58
|
-
const r2 = await mkfashion.init({ projectId, identifier: validSku })
|
|
59
|
-
log(
|
|
60
|
-
'init() sem target → ok: false, reason: missing_target',
|
|
61
|
-
r2.ok === false && r2.reason === 'missing_target' && r2.element === null,
|
|
62
|
-
fmt(r2)
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
// ---- TEST 3 (skipped): init sem projectId ----
|
|
66
|
-
// Comportamento atual: _resolveProjectId silenciosamente faz fallback pro
|
|
67
|
-
// projeto Gregory quando raw é falsy. Conhecido como débito técnico,
|
|
68
|
-
// não testamos como "missing_projectId" aqui.
|
|
69
|
-
|
|
70
|
-
// ---- TEST 4: init sem identifier ----
|
|
71
|
-
const r4 = await mkfashion.init({ projectId, target: '#target-2' })
|
|
72
|
-
log(
|
|
73
|
-
'init() sem identifier → ok: false, reason: missing_identifier',
|
|
74
|
-
r4.ok === false && r4.reason === 'missing_identifier' && r4.element === null,
|
|
75
|
-
fmt(r4)
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
// ---- TEST 5: init com target inexistente ----
|
|
79
|
-
const r5 = await mkfashion.init({
|
|
80
|
-
projectId,
|
|
81
|
-
identifier: validSku,
|
|
82
|
-
target: '#nao-existe'
|
|
83
|
-
})
|
|
84
|
-
log(
|
|
85
|
-
'init() com target inexistente → ok: false, reason: target_not_found',
|
|
86
|
-
r5.ok === false && r5.reason === 'target_not_found' && r5.element === null,
|
|
87
|
-
fmt(r5)
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
// ---- TEST 6: init com SKU indisponível ----
|
|
91
|
-
const r6 = await mkfashion.init({
|
|
92
|
-
projectId,
|
|
93
|
-
identifier: invalidSku,
|
|
94
|
-
target: '#target-2'
|
|
95
|
-
})
|
|
96
|
-
log(
|
|
97
|
-
'init() com SKU indisponível → ok: false, reason: unavailable',
|
|
98
|
-
r6.ok === false && r6.reason === 'unavailable' && r6.element === null,
|
|
99
|
-
fmt(r6)
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
// ---- TEST 7: addToCart callback é disparado via postMessage ----
|
|
103
|
-
let cartPayload = null
|
|
104
|
-
mkfashion.addToCart(payload => { cartPayload = payload })
|
|
105
|
-
|
|
106
|
-
// Abre o modal pra acionar o setup do message listener
|
|
107
|
-
await mkfashion.open({ projectId, identifier: validSku })
|
|
108
|
-
// Fecha imediatamente (listener fica armado)
|
|
109
|
-
mkfashion.close()
|
|
110
|
-
|
|
111
|
-
// Simula iframe enviando add_to_cart
|
|
112
|
-
const fakePayload = {
|
|
113
|
-
size: 'M',
|
|
114
|
-
product: {
|
|
115
|
-
name: 'Afrik - Óculos de Sol',
|
|
116
|
-
price: 299.90,
|
|
117
|
-
variantSku: 'SKU-VAR-M',
|
|
118
|
-
produtoVarianteId: '12345',
|
|
119
|
-
productUrl: 'https://example.com/produto/afrik',
|
|
120
|
-
tryonImageUrl: 'https://cdn.example.com/tryon/abc.jpg'
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
window.postMessage({
|
|
124
|
-
source: 'mkfashion-app',
|
|
125
|
-
action: 'add_to_cart',
|
|
126
|
-
data: fakePayload
|
|
127
|
-
}, '*')
|
|
128
|
-
|
|
129
|
-
// Aguarda o handler async processar
|
|
130
|
-
await new Promise(r => setTimeout(r, 100))
|
|
131
|
-
|
|
132
|
-
log(
|
|
133
|
-
'addToCart callback dispara com payload simplificado (não-Gregory)',
|
|
134
|
-
cartPayload !== null
|
|
135
|
-
&& cartPayload.mainIdentifier === validSku
|
|
136
|
-
&& cartPayload.selectedIdentifier === 'SKU-VAR-M'
|
|
137
|
-
&& cartPayload.name === 'Afrik - Óculos de Sol'
|
|
138
|
-
&& cartPayload.price === 299.90
|
|
139
|
-
&& cartPayload.selectedSize === 'M'
|
|
140
|
-
&& cartPayload.productUrl === 'https://example.com/produto/afrik',
|
|
141
|
-
fmt(cartPayload)
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
// ---- TEST 8: onInteraction callback ----
|
|
145
|
-
let interactionData = null
|
|
146
|
-
mkfashion.onInteraction(data => { interactionData = data })
|
|
147
|
-
|
|
148
|
-
window.postMessage({
|
|
149
|
-
source: 'mkfashion-app',
|
|
150
|
-
action: 'interaction',
|
|
151
|
-
data: { category: 'try_on', action: 'photo_uploaded' }
|
|
152
|
-
}, '*')
|
|
153
|
-
|
|
154
|
-
await new Promise(r => setTimeout(r, 100))
|
|
155
|
-
|
|
156
|
-
log(
|
|
157
|
-
'onInteraction callback dispara com category + action',
|
|
158
|
-
interactionData !== null
|
|
159
|
-
&& interactionData.category === 'try_on'
|
|
160
|
-
&& interactionData.action === 'photo_uploaded',
|
|
161
|
-
fmt(interactionData)
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
// ---- Marker pro screenshot saber que terminou ----
|
|
165
|
-
document.title = 'DONE — ' + output.querySelectorAll('.test').length + ' tests'
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (typeof mkfashion !== 'undefined') {
|
|
169
|
-
run().catch(e => log('Exception não tratada', false, e.message + '\n' + e.stack))
|
|
170
|
-
} else {
|
|
171
|
-
document.addEventListener('DOMContentLoaded', run)
|
|
172
|
-
}
|
|
173
|
-
</script>
|
|
174
|
-
</body>
|
|
175
|
-
</html>
|
package/test-responsive.html
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="pt-BR">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<title>Test — Responsividade do botão</title>
|
|
6
|
-
<script src="./src/mkfashion.js"></script>
|
|
7
|
-
<style>
|
|
8
|
-
body { font-family: system-ui, sans-serif; margin: 24px; }
|
|
9
|
-
h3 { margin: 20px 0 8px; font-size: 13px; color: #444; }
|
|
10
|
-
.box { background: #fef3c7; padding: 8px; margin-bottom: 8px; }
|
|
11
|
-
</style>
|
|
12
|
-
</head>
|
|
13
|
-
<body>
|
|
14
|
-
|
|
15
|
-
<h3>Container 800px (mais largo que o max-width)</h3>
|
|
16
|
-
<div class="box" style="width: 800px;">
|
|
17
|
-
<div id="c1"></div>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<h3>Container 300px (menor que 402px do card)</h3>
|
|
21
|
-
<div class="box" style="width: 300px;">
|
|
22
|
-
<div id="c2"></div>
|
|
23
|
-
</div>
|
|
24
|
-
|
|
25
|
-
<h3>Container 180px (menor que 238px do simples)</h3>
|
|
26
|
-
<div class="box" style="width: 180px;">
|
|
27
|
-
<div id="c3"></div>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<h3>Container sem width (herda do pai)</h3>
|
|
31
|
-
<div class="box">
|
|
32
|
-
<div id="c4"></div>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<script>
|
|
36
|
-
var pid = '69c56d5373ecdf64df48e330'
|
|
37
|
-
var sku = '000756397001'
|
|
38
|
-
// Configura os 4 inits — todos pra mesma config (gregory-card no momento)
|
|
39
|
-
mkfashion.init({ projectId: pid, identifier: sku, target: '#c1' })
|
|
40
|
-
mkfashion.init({ projectId: pid, identifier: sku, target: '#c2' })
|
|
41
|
-
mkfashion.init({ projectId: pid, identifier: sku, target: '#c3' })
|
|
42
|
-
mkfashion.init({ projectId: pid, identifier: sku, target: '#c4' })
|
|
43
|
-
</script>
|
|
44
|
-
</body>
|
|
45
|
-
</html>
|