mkfashion-sdk 2.4.6 → 2.4.7

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/index.html CHANGED
@@ -1,94 +1,79 @@
1
- <!DOCTYPE html>
2
- <html lang="pt-BR">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Provador Virtual</title>
8
- <script src="https://unpkg.com/mkfashion-sdk/src/mkfashion.js"></script>
9
- </head>
10
-
11
- <body>
12
-
13
- <button id="btn-provar" style="display: none;">Provar Virtualmente</button>
14
-
15
- <script>
16
- (function () {
17
- var projectid = '69f09de7fe3120f54c06e0d6';
18
- var identifier = 'demo-002';
19
-
20
- function initMkFashion() {
21
- mkfashion.addToCart(function (payload) {
22
- console.log('Adicionando ao carrinho:', payload);
23
-
24
- var variantSku = payload.selectedIdentifier;
25
-
26
- fetch('/cart/add.js', {
27
- method: 'POST',
28
- headers: {
29
- 'Content-Type': 'application/json',
30
- },
31
- body: JSON.stringify({
32
- items: [{
33
- quantity: 1,
34
- id: variantSku
35
- }]
36
- })
37
- })
38
- .then(function (response) {
39
- return response.json();
40
- })
41
- .then(function (data) {
42
- console.log('Produto adicionado:', data);
43
- // document.dispatchEvent(new CustomEvent('cart:updated'));
44
- })
45
- .catch(function (error) {
46
- console.error('Erro carrinho:', error);
47
- });
48
- });
49
-
50
- mkfashion.onInteraction((data) => {
51
- console.log(data.category, data.action)
52
- })
53
-
54
- mkfashion.isAvailable(projectid, identifier)
55
- .then(function (disponivel) {
56
- if (disponivel) {
57
- var button = document.getElementById('btn-provar');
58
-
59
- if (button) {
60
- button.style.display = 'inline-block';
61
-
62
- button.onclick = function () {
63
- mkfashion.open({
64
- projectid: projectid,
65
- identifier: identifier
66
- });
67
- };
68
- }
69
- }
70
- })
71
- .catch(function (error) {
72
- console.error('Erro disponibilidade MK Fashion:', error);
73
- });
74
- }
75
-
76
- function waitForMkFashion() {
77
- if (typeof mkfashion !== 'undefined') {
78
- initMkFashion();
79
- } else {
80
- setTimeout(waitForMkFashion, 100);
81
- }
82
- }
83
-
84
- if (document.readyState === "loading") {
85
- document.addEventListener("DOMContentLoaded", waitForMkFashion);
86
- } else {
87
- waitForMkFashion();
88
- }
89
- })();
90
- </script>
91
-
92
- </body>
93
-
94
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Provador Virtual</title>
8
+ <script src="./src/mkfashion.js"></script>
9
+ </head>
10
+
11
+ <body>
12
+
13
+ <div id="mkfashion-container"></div>
14
+
15
+ <script>
16
+ (function () {
17
+ var projectid = '69f9e5b0f4eb0c4403319830';
18
+ var identifier = '1881';
19
+
20
+ function initMkFashion() {
21
+ mkfashion.addToCart(function (payload) {
22
+ console.log('Adicionando ao carrinho:', payload);
23
+
24
+ var variantSku = payload.selectedIdentifier;
25
+
26
+ fetch('/cart/add.js', {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ },
31
+ body: JSON.stringify({
32
+ items: [{
33
+ quantity: 1,
34
+ id: variantSku
35
+ }]
36
+ })
37
+ })
38
+ .then(function (response) {
39
+ return response.json();
40
+ })
41
+ .then(function (data) {
42
+ console.log('Produto adicionado:', data);
43
+ // document.dispatchEvent(new CustomEvent('cart:updated'));
44
+ })
45
+ .catch(function (error) {
46
+ console.error('Erro carrinho:', error);
47
+ });
48
+ });
49
+
50
+ mkfashion.onInteraction(function (data) {
51
+ console.log(data.category, data.action);
52
+ });
53
+
54
+ mkfashion.init({
55
+ projectId: projectid,
56
+ identifier: identifier,
57
+ target: '#mkfashion-container'
58
+ });
59
+ }
60
+
61
+ function waitForMkFashion() {
62
+ if (typeof mkfashion !== 'undefined') {
63
+ initMkFashion();
64
+ } else {
65
+ setTimeout(waitForMkFashion, 100);
66
+ }
67
+ }
68
+
69
+ if (document.readyState === "loading") {
70
+ document.addEventListener("DOMContentLoaded", waitForMkFashion);
71
+ } else {
72
+ waitForMkFashion();
73
+ }
74
+ })();
75
+ </script>
76
+
77
+ </body>
78
+
79
+ </html>
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mkfashion-sdk",
3
- "version": "2.4.6",
3
+ "version": "2.4.7",
4
4
  "description": "SDK para integrar o provador virtual mKFashion com suporte a Wake Commerce",
5
5
  "main": "src/mkfashion.js",
6
6
  "scripts": {
package/src/mkfashion.js CHANGED
@@ -128,11 +128,6 @@ const mkfashion = {
128
128
  return
129
129
  }
130
130
 
131
- // Avisa o iframe pra limpar a UI de confirmacao de saida antes de esconder.
132
- // Sem isso, o modal de "Tem certeza?" fica aberto e reaparece na proxima abertura
133
- // (porque o iframe nao eh destruido entre open/close — apenas escondido).
134
- this._postToIframe('dismiss_exit_confirmation')
135
-
136
131
  this._hideModal()
137
132
  this._triggerCallback('onClose')
138
133
  this._isOpen = false
@@ -395,6 +390,8 @@ const mkfashion = {
395
390
  }
396
391
  },
397
392
 
393
+ // init: ver seção "BOTÃO TRY-ON" mais abaixo
394
+
398
395
  // ============ METODOS PRIVADOS ============
399
396
 
400
397
  _log(message, data = null) {
@@ -772,6 +769,368 @@ const mkfashion = {
772
769
  this._messageHandler = null
773
770
  this._log('Listener de mensagens removido')
774
771
  }
772
+ },
773
+
774
+ // ============================================================
775
+ // ============ BOTÃO TRY-ON ============
776
+ // ============================================================
777
+ // Spec: mk-fashion-cms/docs/tryon-button-spec.md
778
+ //
779
+ // Fluxo:
780
+ // init(opts)
781
+ // → fetch availability + template (paralelo)
782
+ // → _normalizeButtonConfig (escolhe desktop/mobile + aplica defaults)
783
+ // → dispatch via _BUTTON_RENDERERS[config.style] → renderer (gregory-black ou gregory-card)
784
+ // → renderer constrói árvore DOM via _el + helpers de ícone/badge
785
+ // → appendChild no target
786
+ //
787
+ // Sub-seções:
788
+ // 1. Entry point (público): init
789
+ // 2. Configuração visual: constantes e defaults
790
+ // 3. Ícones: SVGs hardcoded
791
+ // 4. Helpers genéricos: _el, ícones, borda, fonte
792
+ // 5. Camada de dados: fetch e normalização do template
793
+ // 6. Preset gregory-black: badge + botão simples
794
+ // 7. Preset gregory-card: card completo com badge + cta
795
+
796
+ // --- 1. Entry point (público) ---
797
+
798
+ /**
799
+ * Inicializa o provador virtual: valida disponibilidade, busca o template visual,
800
+ * renderiza o botão (gregory-black ou gregory-card) e faz wiring do clique → open().
801
+ *
802
+ * @param {Object} options
803
+ * @param {string} options.projectId - ID do projeto (aliases: projectid, store)
804
+ * @param {string} options.identifier - Identificador do produto (alias: sku)
805
+ * @param {string} options.target - Seletor CSS do container (ex: '#produto', '.actions')
806
+ * @returns {Promise<{ok: boolean, reason: string, element: HTMLElement|null}>}
807
+ * - `ok`: true se o botão foi renderizado.
808
+ * - `reason`: 'rendered' (sucesso) | 'unavailable' (produto sem try-on) |
809
+ * 'missing_projectId' | 'missing_identifier' | 'missing_target' |
810
+ * 'target_not_found'.
811
+ * - `element`: o elemento DOM renderizado (ou null em caso de falha).
812
+ *
813
+ * @example
814
+ * const { ok, reason } = await mkfashion.init({ projectId, identifier, target })
815
+ * if (!ok) {
816
+ * if (reason === 'unavailable') alert('Esse produto não tem prova virtual')
817
+ * }
818
+ */
819
+ async init(options = {}) {
820
+ const fail = reason => ({ ok: false, reason, element: null })
821
+
822
+ const raw = options.projectId || options.projectid || options.store
823
+ const projectId = this._resolveProjectId(raw)
824
+ const identifier = (options.identifier || options.sku || '').trim()
825
+ const target = options.target
826
+
827
+ if (!projectId) {
828
+ console.error('[mKFashion] projectId e obrigatorio')
829
+ return fail('missing_projectId')
830
+ }
831
+ if (!identifier) {
832
+ console.error('[mKFashion] identifier e obrigatorio')
833
+ return fail('missing_identifier')
834
+ }
835
+ if (!target) {
836
+ console.error('[mKFashion] target e obrigatorio')
837
+ return fail('missing_target')
838
+ }
839
+
840
+ const container = document.querySelector(target)
841
+ if (!container) {
842
+ console.error(`[mKFashion] Elemento não encontrado: ${target}`)
843
+ return fail('target_not_found')
844
+ }
845
+
846
+ const [available, template] = await Promise.all([
847
+ this.isAvailable(projectId, identifier).catch(() => false),
848
+ this._fetchButtonTemplate(projectId)
849
+ ])
850
+
851
+ if (!available) {
852
+ this._log('Produto indisponível, botão não criado', { projectId, identifier })
853
+ return fail('unavailable')
854
+ }
855
+
856
+ const isMobile = window.matchMedia('(max-width: 767px)').matches
857
+ const config = this._normalizeButtonConfig(template, isMobile)
858
+ const renderer = this._BUTTON_RENDERERS[config.style] || this._BUTTON_RENDERERS['gregory-black']
859
+
860
+ this._ensureJakartaFont()
861
+ const element = renderer.call(this, config, projectId, identifier)
862
+
863
+ container.appendChild(element)
864
+ this._log('Botão criado', { target, projectId, identifier, style: config.style })
865
+ return { ok: true, reason: 'rendered', element }
866
+ },
867
+
868
+ // --- 2. Configuração visual ---
869
+
870
+ _FONT_STACK: "'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
871
+ _BUTTON_RADIUS: { none: '0', sm: '4px', md: '8px', pill: '9999px' },
872
+ _CARD_RADIUS: { none: '0', sm: '4px', md: '8px', lg: '16px' },
873
+
874
+ _BUTTON_DEFAULTS: {
875
+ style: 'gregory-black',
876
+ text: 'Provar Virtualmente',
877
+ fontSize: 14,
878
+ subtext: '',
879
+ bgColor: '#1E1E1E',
880
+ textColor: '#FFFFFF',
881
+ badgeBgColor: '#C51A1B',
882
+ badgeTextColor: '#FFFFFF',
883
+ borderRadius: 'md',
884
+ borderColor: '#1E1E1E',
885
+ borderWidth: 0,
886
+ cardTitle: '',
887
+ cardDescription: '',
888
+ cardFooter: '',
889
+ cardBgColor: '#F6F6F6',
890
+ cardTitleColor: '#1E1E1E',
891
+ cardTextColor: '#717171',
892
+ cardBorderRadius: 'md'
893
+ },
894
+
895
+ _BUTTON_RENDERERS: {
896
+ 'gregory-black': function (c, p, i) { return this._renderSimpleButton(c, p, i) },
897
+ 'gregory-card': function (c, p, i) { return this._renderCardButton(c, p, i) }
898
+ },
899
+
900
+ // --- 3. Ícones (assets visuais hardcoded) ---
901
+
902
+ _CAMERA_ICON_SVG: '<svg viewBox="0 0 17.5263 20.6862" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;width:100%;height:100%"><g><path d="M8.49995 12.1862C10.1568 12.1862 11.5 10.8431 11.5 9.18622C11.5 7.52936 10.1568 6.18622 8.49995 6.18622C6.8431 6.18622 5.49995 7.52936 5.49995 9.18622C5.49995 10.8431 6.8431 12.1862 8.49995 12.1862Z" stroke="currentColor" shape-rendering="crispEdges"/><path d="M0.5 9.11152C0.5 6.56953 0.5 5.29894 1.1099 4.38664C1.375 3.9902 1.71424 3.65088 2.10821 3.38809C2.6945 2.9958 3.42899 2.85564 4.55351 2.80588C5.09013 2.80588 5.55183 2.39949 5.65687 1.87284C5.73715 1.48708 5.94572 1.14138 6.24735 0.894155C6.54898 0.646934 6.92516 0.51336 7.31231 0.516006H9.9734C10.7779 0.516006 11.4709 1.08412 11.6288 1.87284C11.7339 2.39949 12.1956 2.80588 12.7322 2.80588C13.8559 2.85564 12.9129 4.67413 13.5 5.06559C13.8949 5.32932 15.1484 3.80355 15.5 4.02647C16.1099 4.93876 16.7857 6.56953 16.7857 9.11152C16.7857 11.6535 16.7857 12.9233 16.1758 13.8364C15.9107 14.2328 15.5715 14.5722 15.1775 14.835C14.2647 15.4445 12.9936 15.4445 10.4522 15.4445H6.83351C4.29213 15.4445 3.02103 15.4445 2.10821 14.835C1.71446 14.5718 1.3755 14.2322 1.11071 13.8356C0.933802 13.5672 0.803519 13.2698 0.725557 12.9564" stroke="currentColor" stroke-linecap="round"/><path d="M12.676 0.607931C12.9446 -0.178095 14.0307 -0.2019 14.3491 0.536515L14.3761 0.60838L14.7385 1.66839C14.8216 1.91149 14.9559 2.13395 15.1322 2.32076C15.3086 2.50757 15.5229 2.65439 15.7608 2.75131L15.8583 2.78769L16.9183 3.14971C17.7044 3.41831 17.7282 4.50437 16.9902 4.82283L16.9183 4.84978L15.8583 5.21225C15.6151 5.29526 15.3926 5.42947 15.2057 5.60582C15.0188 5.78218 14.8719 5.99657 14.7749 6.23453L14.7385 6.33155L14.3765 7.39201C14.1079 8.17803 13.0219 8.20184 12.7039 7.46387L12.676 7.39201L12.314 6.332C12.231 6.08881 12.0968 5.86627 11.9204 5.67938C11.744 5.49249 11.5297 5.3456 11.2917 5.24863L11.1947 5.21225L10.1347 4.85022C9.34817 4.58163 9.32437 3.49557 10.0628 3.17756L10.1347 3.14971L11.1947 2.78769C11.4378 2.70462 11.6602 2.57039 11.847 2.39404C12.0339 2.21769 12.1807 2.00333 12.2776 1.76541L12.314 1.66839L12.676 0.607931Z" fill="currentColor"/></g></svg>',
903
+
904
+ _SPARKLE_ICON_SVG: '<svg viewBox="0 0 11.6122 12.7248" fill="none" xmlns="http://www.w3.org/2000/svg" style="display:block;width:12px;height:12px"><path d="M4.09055 2.01133C4.43939 0.9905 5.84989 0.959583 6.26347 1.91858L6.29847 2.01192L6.76922 3.38858C6.8771 3.7043 7.05144 3.99321 7.28047 4.23583C7.5095 4.47845 7.7879 4.66912 8.09689 4.795L8.22347 4.84225L9.60014 5.31242C10.621 5.66125 10.6519 7.07175 9.69347 7.48533L9.60014 7.52033L8.22347 7.99108C7.90764 8.09889 7.61862 8.2732 7.3759 8.50223C7.13318 8.73127 6.94241 9.0097 6.81647 9.31875L6.76922 9.44475L6.29905 10.822C5.95022 11.8428 4.53972 11.8737 4.12672 10.9153L4.09055 10.822L3.62039 9.44533C3.51258 9.12951 3.33827 8.84048 3.10924 8.59776C2.8802 8.35504 2.60177 8.16427 2.29272 8.03833L2.16672 7.99108L0.790054 7.52092C-0.231363 7.17208 -0.26228 5.76158 0.69672 5.34858L0.790054 5.31242L2.16672 4.84225C2.48244 4.73437 2.77135 4.56003 3.01397 4.331C3.25658 4.10197 3.44726 3.82357 3.57314 3.51458L3.62039 3.38858L4.09055 2.01133ZM9.86147 0C9.9706 0 10.0775 0.0306123 10.1701 0.0883584C10.2627 0.146104 10.3373 0.228668 10.3853 0.326667L10.4133 0.394917L10.6175 0.993417L11.2166 1.19758C11.3259 1.23474 11.4218 1.30353 11.492 1.39523C11.5623 1.48693 11.6037 1.59741 11.611 1.71268C11.6184 1.82795 11.5914 1.94281 11.5334 2.04271C11.4755 2.14261 11.3891 2.22305 11.2854 2.27383L11.2166 2.30183L10.6181 2.506L10.4139 3.10508C10.3767 3.21442 10.3078 3.31024 10.2161 3.38041C10.1244 3.45058 10.0139 3.49194 9.89859 3.49925C9.78333 3.50656 9.66849 3.47949 9.56863 3.42147C9.46876 3.36345 9.38837 3.27709 9.33764 3.17333L9.30964 3.10508L9.10547 2.50658L8.50639 2.30242C8.39702 2.26526 8.30115 2.19647 8.23092 2.10477C8.16069 2.01307 8.11926 1.90259 8.11189 1.78732C8.10452 1.67205 8.13153 1.55719 8.18951 1.45729C8.24749 1.35739 8.33381 1.27695 8.43755 1.22617L8.50639 1.19817L9.10489 0.994L9.30905 0.394917C9.34839 0.279665 9.42281 0.179613 9.52187 0.108791C9.62094 0.0379686 9.73969 -7.30808e-05 9.86147 0Z" fill="currentColor"/></svg>',
905
+
906
+ // --- 4. Helpers genéricos ---
907
+
908
+ /** Cria elemento DOM declarativamente: el('tag', { style, text|html, children, on, attrs }) */
909
+ _el(tag, opts = {}) {
910
+ const node = document.createElement(tag)
911
+ if (opts.style) node.style.cssText = opts.style
912
+ if (opts.text != null) node.textContent = opts.text
913
+ if (opts.html != null) node.innerHTML = opts.html
914
+ if (opts.attrs) for (const k in opts.attrs) node.setAttribute(k, opts.attrs[k])
915
+ if (opts.on) for (const k in opts.on) node.addEventListener(k, opts.on[k])
916
+ if (opts.children) for (const c of opts.children) if (c) node.appendChild(c)
917
+ return node
918
+ },
919
+
920
+ _borderFromConfig(c) {
921
+ return c.borderWidth > 0 && c.borderColor ? `${c.borderWidth}px solid ${c.borderColor}` : 'none'
922
+ },
923
+
924
+ _ensureJakartaFont() {
925
+ if (document.getElementById('mkfashion-jakarta-font')) return
926
+ document.head.appendChild(this._el('link', {
927
+ attrs: {
928
+ id: 'mkfashion-jakarta-font',
929
+ rel: 'stylesheet',
930
+ href: 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap'
931
+ }
932
+ }))
933
+ },
934
+
935
+ _renderCameraIcon(color) {
936
+ return this._el('span', {
937
+ style: `width: 24px; height: 24px; padding: 2px; box-sizing: border-box;
938
+ display: inline-flex; align-items: center; justify-content: center;
939
+ color: ${color}; flex-shrink: 0;`,
940
+ html: this._CAMERA_ICON_SVG
941
+ })
942
+ },
943
+
944
+ _renderSparkleIcon(color) {
945
+ return this._el('span', {
946
+ style: `width: 12px; height: 12px; display: inline-flex; align-items: center;
947
+ justify-content: center; color: ${color}; flex-shrink: 0;`,
948
+ html: this._SPARKLE_ICON_SVG
949
+ })
950
+ },
951
+
952
+ // --- 5. Camada de dados (API + normalização) ---
953
+
954
+ async _fetchButtonTemplate(projectId) {
955
+ try {
956
+ const response = await fetch(`${this.apiUrl}/projects/${projectId}`)
957
+ if (!response.ok) throw new Error(`Erro na API: ${response.status}`)
958
+ const data = await response.json()
959
+ const tpl = data?.project?.template?.tryOnButton || data?.template?.tryOnButton || null
960
+ this._log('Template recebido', tpl)
961
+ return tpl
962
+ } catch (error) {
963
+ console.error('[mKFashion] Erro ao buscar template do botão:', error)
964
+ return null
965
+ }
966
+ },
967
+
968
+ /** Escolhe config desktop/mobile, força gregory-black em mobile, aplica defaults, pré-resolve px. */
969
+ _normalizeButtonConfig(template, isMobile) {
970
+ const raw = (isMobile ? template?.mobile : template?.desktop)
971
+ || template?.desktop || template?.mobile || {}
972
+ const merged = { ...this._BUTTON_DEFAULTS, ...raw }
973
+ if (merged.style === 'gregory-card' && isMobile) merged.style = 'gregory-black'
974
+ merged.borderRadiusPx = this._BUTTON_RADIUS[merged.borderRadius] ?? '0'
975
+ merged.cardBorderRadiusPx = this._CARD_RADIUS[merged.cardBorderRadius] ?? '0'
976
+ merged.fontSize = Number(merged.fontSize) || 14
977
+ merged.borderWidth = Number(merged.borderWidth) || 0
978
+ return merged
979
+ },
980
+
981
+ // --- 6. Preset gregory-black (botão simples) ---
982
+
983
+ _renderBlackBadge(c) {
984
+ if (!c.subtext) return null
985
+ return this._el('span', {
986
+ style: `display: flex; align-items: center; justify-content: center;
987
+ padding: 4px; background: ${c.badgeBgColor};
988
+ font-family: ${this._FONT_STACK};
989
+ font-weight: 700; font-size: 16px; text-transform: uppercase;
990
+ color: ${c.badgeTextColor}; line-height: 1; white-space: nowrap;`,
991
+ text: c.subtext
992
+ })
993
+ },
994
+
995
+ _renderSimpleButton(c, projectId, identifier) {
996
+ const content = this._el('span', {
997
+ style: 'display: inline-flex; align-items: center; gap: 4px;',
998
+ children: [
999
+ this._renderCameraIcon(c.textColor),
1000
+ this._el('span', {
1001
+ text: c.text,
1002
+ style: `font-family: ${this._FONT_STACK}; font-weight: 700;
1003
+ font-size: ${c.fontSize}px; text-transform: uppercase;
1004
+ letter-spacing: 2px; color: ${c.textColor};
1005
+ white-space: nowrap; text-align: center; line-height: 1;`
1006
+ })
1007
+ ]
1008
+ })
1009
+
1010
+ return this._el('button', {
1011
+ attrs: { type: 'button' },
1012
+ style: `display: inline-flex; align-items: center; justify-content: center;
1013
+ gap: 8px; padding: 8px; width: 238px; box-sizing: border-box;
1014
+ background: ${c.bgColor};
1015
+ border: ${this._borderFromConfig(c)};
1016
+ border-radius: ${c.borderRadiusPx};
1017
+ font-family: ${this._FONT_STACK};
1018
+ cursor: pointer; transition: opacity 0.2s ease; line-height: 1;
1019
+ -webkit-appearance: none; appearance: none;`,
1020
+ children: [content, this._renderBlackBadge(c)],
1021
+ on: {
1022
+ mouseenter: e => { e.currentTarget.style.opacity = '0.85' },
1023
+ mouseleave: e => { e.currentTarget.style.opacity = '1' },
1024
+ click: () => this.open({ projectId, identifier })
1025
+ }
1026
+ })
1027
+ },
1028
+
1029
+ // --- 7. Preset gregory-card (card com title + descrição + cta) ---
1030
+
1031
+ _renderCardBadge(c) {
1032
+ if (!c.subtext) return null
1033
+ return this._el('span', {
1034
+ style: `display: flex; align-items: center; gap: 4px; padding: 4px;
1035
+ background: ${c.badgeBgColor};
1036
+ font-family: ${this._FONT_STACK};
1037
+ font-weight: 600; font-size: 10px; text-transform: uppercase;
1038
+ color: ${c.badgeTextColor}; line-height: 1; white-space: nowrap;`,
1039
+ children: [
1040
+ this._renderSparkleIcon(c.badgeTextColor),
1041
+ this._el('span', { text: c.subtext })
1042
+ ]
1043
+ })
1044
+ },
1045
+
1046
+ _renderCardCta(c, projectId, identifier) {
1047
+ return this._el('button', {
1048
+ attrs: { type: 'button' },
1049
+ style: `display: flex; align-items: center; justify-content: center;
1050
+ gap: 8px; padding: 8px 12px 8px 8px;
1051
+ width: 100%; margin-top: auto; box-sizing: border-box;
1052
+ background: ${c.bgColor};
1053
+ border: ${this._borderFromConfig(c)};
1054
+ border-radius: ${c.borderRadiusPx};
1055
+ filter: drop-shadow(0 0 6px rgba(0,0,0,0.08));
1056
+ font-family: ${this._FONT_STACK};
1057
+ cursor: pointer; transition: opacity 0.2s ease; line-height: 1;
1058
+ -webkit-appearance: none; appearance: none;`,
1059
+ children: [
1060
+ this._renderCameraIcon(c.textColor),
1061
+ this._el('span', {
1062
+ text: c.text,
1063
+ style: `font-family: ${this._FONT_STACK}; font-weight: 700;
1064
+ font-size: ${c.fontSize}px; color: ${c.textColor};
1065
+ white-space: nowrap; line-height: 1;`
1066
+ })
1067
+ ],
1068
+ on: {
1069
+ mouseenter: e => { e.currentTarget.style.opacity = '0.85' },
1070
+ mouseleave: e => { e.currentTarget.style.opacity = '1' },
1071
+ click: () => this.open({ projectId, identifier })
1072
+ }
1073
+ })
1074
+ },
1075
+
1076
+ _renderCardButton(c, projectId, identifier) {
1077
+ const titleBlock = this._el('div', {
1078
+ style: 'display: flex; flex-direction: column; gap: 4px;',
1079
+ children: [
1080
+ c.cardTitle && this._el('p', {
1081
+ text: c.cardTitle,
1082
+ style: `margin: 0; font-family: ${this._FONT_STACK};
1083
+ font-weight: 700; font-size: 16px; letter-spacing: 1px;
1084
+ text-transform: uppercase; color: ${c.cardTitleColor};
1085
+ line-height: 1.2;`
1086
+ }),
1087
+ c.cardDescription && this._el('p', {
1088
+ html: c.cardDescription,
1089
+ style: `margin: 0; font-family: ${this._FONT_STACK};
1090
+ font-weight: 400; font-size: 12px; max-width: 177px;
1091
+ line-height: 1.2; color: ${c.cardTextColor};`
1092
+ })
1093
+ ]
1094
+ })
1095
+
1096
+ const details = this._el('div', {
1097
+ style: `display: flex; flex-direction: column; justify-content: space-between;
1098
+ gap: 16px; flex: 1 1 0; min-width: 0;`,
1099
+ children: [
1100
+ titleBlock,
1101
+ c.cardFooter && this._el('p', {
1102
+ text: c.cardFooter,
1103
+ style: `margin: 0; font-family: ${this._FONT_STACK};
1104
+ font-weight: 400; font-size: 10px; color: ${c.cardTextColor};
1105
+ line-height: 1.2;`
1106
+ })
1107
+ ]
1108
+ })
1109
+
1110
+ const right = this._el('div', {
1111
+ style: `display: flex; flex-direction: column; align-items: flex-end;
1112
+ flex: 1 1 0; min-width: 0;`,
1113
+ children: [
1114
+ this._renderCardBadge(c),
1115
+ this._renderCardCta(c, projectId, identifier)
1116
+ ]
1117
+ })
1118
+
1119
+ const row = this._el('div', {
1120
+ style: `display: flex; gap: 16px; align-items: stretch;
1121
+ width: 100%; flex: 1; box-sizing: border-box;`,
1122
+ children: [details, right]
1123
+ })
1124
+
1125
+ return this._el('div', {
1126
+ style: `display: flex; flex-direction: column; align-items: flex-start;
1127
+ padding: 12px; width: 402px; min-height: 140px;
1128
+ box-sizing: border-box;
1129
+ background: ${c.cardBgColor};
1130
+ border-radius: ${c.cardBorderRadiusPx};
1131
+ font-family: ${this._FONT_STACK};`,
1132
+ children: [row]
1133
+ })
775
1134
  }
776
1135
  }
777
1136
 
package/test-e2e.html ADDED
@@ -0,0 +1,175 @@
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>