@wewear/virtual-try-on 1.4.8 → 1.4.10

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/dist/index.js CHANGED
@@ -4,62 +4,16 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WeWearVirtualTryOn = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- /** CSS class names for consistent styling */
8
7
  const CSS_CLASSES = {
9
8
  BUTTON_CONTAINER: "wewear-vto-button-container",
10
9
  BUTTON: "wewear-vto-button",
11
10
  MODAL: "wewear-vto-modal",
12
- PREVIEW_BADGE: "ww-vto-preview-badge",
13
11
  };
14
- /** Z-index values for proper layering */
15
12
  const Z_INDEX = {
16
13
  BUTTON: 10,
17
14
  MODAL: 99999,
18
15
  };
19
16
 
20
- function createPreviewBadge() {
21
- const badge = document.createElement("div");
22
- badge.className = CSS_CLASSES.PREVIEW_BADGE;
23
- badge.innerText = "Enhancing result...";
24
- const styleId = "ww-vto-badge-styles";
25
- if (!document.getElementById(styleId)) {
26
- const styles = document.createElement("style");
27
- styles.id = styleId;
28
- styles.innerHTML = `
29
- .${CSS_CLASSES.PREVIEW_BADGE} {
30
- position: absolute;
31
- top: 20px;
32
- right: 20px;
33
- background: rgba(31, 41, 55, 0.9);
34
- backdrop-filter: blur(4px);
35
- -webkit-backdrop-filter: blur(4px);
36
- color: white;
37
- padding: 6px 14px;
38
- border-radius: 6px;
39
- font-size: 12px;
40
- font-weight: 600;
41
- letter-spacing: 0.5px;
42
- z-index: 10;
43
- pointer-events: none;
44
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
45
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
46
- animation: ww-vto-pulse 2s ease-in-out infinite;
47
- }
48
-
49
- @keyframes ww-vto-pulse {
50
- 0%, 100% {
51
- opacity: 0.9;
52
- }
53
- 50% {
54
- opacity: 1;
55
- }
56
- }
57
- `;
58
- document.head.appendChild(styles);
59
- }
60
- return badge;
61
- }
62
-
63
17
  function getPositionStyles(position) {
64
18
  switch (position) {
65
19
  case "bottom-left":
@@ -83,117 +37,135 @@
83
37
  const container = document.createElement("div");
84
38
  container.className = `${CSS_CLASSES.BUTTON_CONTAINER} ww-button-group`;
85
39
  const positionStyles = getPositionStyles(buttonPosition);
86
- container.style.cssText = `
87
- position: absolute;
88
- ${positionStyles}
89
- display: flex;
90
- gap: 4px;
91
- border-radius: 100px;
92
- background: white;
93
- padding: 4px;
94
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
95
- z-index: ${Z_INDEX.BUTTON};
96
- transition: all 0.2s ease;
40
+ container.style.cssText = `
41
+ position: absolute;
42
+ ${positionStyles}
43
+ display: flex;
44
+ gap: 4px;
45
+ border-radius: 100px;
46
+ background: white;
47
+ padding: 4px;
48
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
49
+ z-index: ${Z_INDEX.BUTTON};
50
+ transition: all 0.2s ease;
97
51
  `;
98
- // Camera button
99
52
  const cameraButton = document.createElement("button");
100
53
  cameraButton.type = "button";
101
54
  cameraButton.className = `${CSS_CLASSES.BUTTON} ww-camera-btn`;
102
55
  cameraButton.setAttribute("aria-label", "Virtual Try-On");
103
- cameraButton.style.cssText = `
104
- display: flex;
105
- cursor: pointer;
106
- align-items: center;
107
- justify-content: center;
108
- border-radius: 100px;
109
- padding: 10px 16px;
110
- border: none;
111
- background: white;
112
- color: #000000;
113
- transition: all 0.2s ease;
114
- flex-shrink: 0;
115
- gap: 8px;
116
- font-size: 14px;
117
- font-weight: 600;
118
- white-space: nowrap;
56
+ if (hasVirtualTryOn) {
57
+ cameraButton.style.cssText = `
58
+ display: flex;
59
+ width: 40px;
60
+ height: 40px;
61
+ cursor: pointer;
62
+ align-items: center;
63
+ justify-content: center;
64
+ border-radius: 100px;
65
+ padding: 0;
66
+ border: none;
67
+ background: white;
68
+ color: #000000;
69
+ transition: all 0.2s ease;
70
+ flex-shrink: 0;
119
71
  `;
120
- cameraButton.innerHTML = `
121
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block; flex-shrink: 0;">
122
- <path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/>
123
- <path d="M20 2v4"/>
124
- <path d="M22 4h-4"/>
125
- <circle cx="4" cy="20" r="2"/>
126
- </svg>
127
- <span style="display: block;">TRY THIS LOOK</span>
72
+ cameraButton.innerHTML = `
73
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
74
+ <path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"></path>
75
+ <circle cx="12" cy="13" r="3"></circle>
76
+ </svg>
128
77
  `;
78
+ }
79
+ else {
80
+ cameraButton.style.cssText = `
81
+ display: flex;
82
+ cursor: pointer;
83
+ align-items: center;
84
+ justify-content: center;
85
+ border-radius: 100px;
86
+ padding: 10px 16px;
87
+ border: none;
88
+ background: white;
89
+ color: #000000;
90
+ transition: all 0.2s ease;
91
+ flex-shrink: 0;
92
+ gap: 8px;
93
+ font-size: 14px;
94
+ font-weight: 600;
95
+ white-space: nowrap;
96
+ `;
97
+ cameraButton.innerHTML = `
98
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block; flex-shrink: 0;">
99
+ <path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/>
100
+ <path d="M20 2v4"/>
101
+ <path d="M22 4h-4"/>
102
+ <circle cx="4" cy="20" r="2"/>
103
+ </svg>
104
+ <span style="display: block;">TRY THIS LOOK</span>
105
+ `;
106
+ }
129
107
  cameraButton.onclick = onCameraClick;
130
108
  container.appendChild(cameraButton);
131
- // Add refresh and toggle buttons if we have a virtual try-on
132
109
  if (hasVirtualTryOn) {
133
- // Refresh button - calls API again
134
110
  if (onRefreshClick) {
135
111
  const refreshButton = document.createElement("button");
136
112
  refreshButton.type = "button";
137
113
  refreshButton.className = "ww-refresh-btn";
138
114
  refreshButton.setAttribute("aria-label", "Refresh virtual try-on");
139
- refreshButton.style.cssText = `
140
- display: flex;
141
- width: 40px;
142
- height: 40px;
143
- cursor: pointer;
144
- align-items: center;
145
- justify-content: center;
146
- border-radius: 100px;
147
- padding: 0;
148
- border: none;
149
- background: white;
150
- color: #333333;
151
- transition: all 0.2s ease;
152
- flex-shrink: 0;
115
+ refreshButton.style.cssText = `
116
+ display: flex;
117
+ width: 40px;
118
+ height: 40px;
119
+ cursor: pointer;
120
+ align-items: center;
121
+ justify-content: center;
122
+ border-radius: 100px;
123
+ padding: 0;
124
+ border: none;
125
+ background: white;
126
+ color: #333333;
127
+ transition: all 0.2s ease;
128
+ flex-shrink: 0;
153
129
  `;
154
- refreshButton.innerHTML = `
155
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
156
- <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
157
- <path d="M3 3v5h5"></path>
158
- <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path>
159
- <path d="M16 16h5v5"></path>
160
- </svg>
130
+ refreshButton.innerHTML = `
131
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
132
+ <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
133
+ <path d="M3 3v5h5"></path>
134
+ <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"></path>
135
+ <path d="M16 16h5v5"></path>
136
+ </svg>
161
137
  `;
162
138
  refreshButton.onclick = onRefreshClick;
163
139
  refreshButton.onclick = onRefreshClick;
164
140
  container.appendChild(refreshButton);
165
141
  }
166
- // Toggle button (scan-face) - switches between original and virtual try-on
167
142
  if (onToggleClick) {
168
143
  const toggleButton = document.createElement("button");
169
144
  toggleButton.type = "button";
170
145
  toggleButton.className = "ww-toggle-btn";
171
146
  toggleButton.setAttribute("aria-label", isShowingVirtualTryOn ? "Show Original Image" : "Show Virtual Try-On");
172
- toggleButton.style.cssText = `
173
- display: flex;
174
- width: 40px;
175
- height: 40px;
176
- cursor: pointer;
177
- align-items: center;
178
- justify-content: center;
179
- border-radius: 100px;
180
- padding: 0;
181
- border: none;
182
- background: ${isShowingVirtualTryOn ? "#333333" : "#e5e7eb"};
183
- color: ${isShowingVirtualTryOn ? "white" : "#333333"};
184
- transition: all 0.2s ease;
185
- flex-shrink: 0;
147
+ toggleButton.style.cssText = `
148
+ display: flex;
149
+ width: 40px;
150
+ height: 40px;
151
+ cursor: pointer;
152
+ align-items: center;
153
+ justify-content: center;
154
+ border-radius: 100px;
155
+ padding: 0;
156
+ border: none;
157
+ background: ${isShowingVirtualTryOn ? "#333333" : "#e5e7eb"};
158
+ color: ${isShowingVirtualTryOn ? "white" : "#333333"};
159
+ transition: all 0.2s ease;
160
+ flex-shrink: 0;
186
161
  `;
187
- toggleButton.innerHTML = `
188
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
189
- <path d="M3 7V5a2 2 0 0 1 2-2h2"></path>
190
- <path d="M17 3h2a2 2 0 0 1 2 2v2"></path>
191
- <path d="M21 17v2a2 2 0 0 1-2 2h-2"></path>
192
- <path d="M7 21H5a2 2 0 0 1-2-2v-2"></path>
193
- <path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
194
- <path d="M9 9h.01"></path>
195
- <path d="M15 9h.01"></path>
196
- </svg>
162
+ toggleButton.innerHTML = `
163
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: block;">
164
+ <path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/>
165
+ <path d="M20 2v4"/>
166
+ <path d="M22 4h-4"/>
167
+ <circle cx="4" cy="20" r="2"/>
168
+ </svg>
197
169
  `;
198
170
  toggleButton.onmouseover = () => {
199
171
  if (isShowingVirtualTryOn) {
@@ -231,114 +203,112 @@
231
203
  return container;
232
204
  }
233
205
 
234
- /**
235
- * Creates a loading overlay that can be shown in different containers
236
- */
237
206
  function createLoadingOverlay(text = "Processing...") {
238
207
  const overlay = document.createElement("div");
239
208
  overlay.className = "ww-loading-overlay";
240
- overlay.style.cssText = `
241
- position: absolute;
242
- top: 0;
243
- left: 0;
244
- width: 100%;
245
- height: 100%;
246
- background: linear-gradient(135deg, rgba(0, 0, 0, 0.92) 0%, rgba(0, 0, 0, 0.88) 100%);
247
- backdrop-filter: blur(8px);
248
- -webkit-backdrop-filter: blur(8px);
249
- display: flex;
250
- flex-direction: column;
251
- align-items: center;
252
- justify-content: center;
253
- z-index: ${Z_INDEX.MODAL + 1};
254
- border-radius: inherit;
255
- animation: ww-fade-in 0.3s ease;
209
+ overlay.style.cssText = `
210
+ position: absolute;
211
+ top: 0;
212
+ left: 0;
213
+ width: 100%;
214
+ height: 100%;
215
+ background: linear-gradient(135deg, rgba(0, 0, 0, 0.92) 0%, rgba(0, 0, 0, 0.88) 100%);
216
+ backdrop-filter: blur(8px);
217
+ -webkit-backdrop-filter: blur(8px);
218
+ display: flex;
219
+ flex-direction: column;
220
+ align-items: center;
221
+ justify-content: center;
222
+ z-index: ${Z_INDEX.MODAL + 1};
223
+ border-radius: inherit;
224
+ animation: ww-fade-in 0.3s ease;
256
225
  `;
257
- // Add CSS animation to the document if not already added
258
226
  if (!document.getElementById("ww-loading-animation")) {
259
227
  const style = document.createElement("style");
260
228
  style.id = "ww-loading-animation";
261
- style.textContent = `
262
- @keyframes ww-fade-in {
263
- from { opacity: 0; }
264
- to { opacity: 1; }
265
- }
266
-
267
- @keyframes ww-pulse-logo {
268
- 0%, 100% {
269
- opacity: 0.8;
270
- transform: scale(1);
271
- }
272
- 50% {
273
- opacity: 1;
274
- transform: scale(1.05);
275
- }
276
- }
277
-
278
- @keyframes ww-spinner-rotate {
279
- 0% { transform: rotate(0deg); }
280
- 100% { transform: rotate(360deg); }
281
- }
282
-
283
- @keyframes ww-dot-bounce {
284
- 0%, 80%, 100% {
285
- transform: scale(0);
286
- opacity: 0.5;
287
- }
288
- 40% {
289
- transform: scale(1);
290
- opacity: 1;
291
- }
292
- }
293
-
294
- .ww-loading-logo {
295
- animation: ww-pulse-logo 2s ease-in-out infinite;
296
- }
297
-
298
- .ww-loading-spinner {
299
- animation: ww-spinner-rotate 1s linear infinite;
300
- }
229
+ style.textContent = `
230
+ @keyframes ww-fade-in {
231
+ from { opacity: 0; }
232
+ to { opacity: 1; }
233
+ }
234
+
235
+ @keyframes ww-pulse-logo {
236
+ 0%, 100% {
237
+ opacity: 0.8;
238
+ transform: scale(1);
239
+ }
240
+ 50% {
241
+ opacity: 1;
242
+ transform: scale(1.05);
243
+ }
244
+ }
245
+
246
+ @keyframes ww-spinner-rotate {
247
+ 0% { transform: rotate(0deg); }
248
+ 100% { transform: rotate(360deg); }
249
+ }
250
+
251
+ @keyframes ww-dot-bounce {
252
+ 0%, 80%, 100% {
253
+ transform: scale(0);
254
+ opacity: 0.5;
255
+ }
256
+ 40% {
257
+ transform: scale(1);
258
+ opacity: 1;
259
+ }
260
+ }
261
+
262
+ .ww-loading-logo {
263
+ animation: ww-pulse-logo 2s ease-in-out infinite;
264
+ }
265
+
266
+ .ww-loading-spinner {
267
+ animation: ww-spinner-rotate 1s linear infinite;
268
+ }
301
269
  `;
302
270
  document.head.appendChild(style);
303
271
  }
304
- overlay.innerHTML = `
305
- <div style="display: flex; flex-direction: column; align-items: center; gap: 32px;">
306
- <div style="position: relative; width: 120px; height: 120px; display: flex; align-items: center; justify-content: center;">
307
- <!-- Outer spinning circle -->
308
- <div class="ww-loading-spinner" style="
309
- position: absolute;
310
- width: 120px;
311
- height: 120px;
312
- border: 3px solid transparent;
313
- border-top: 3px solid #333333;
314
- border-right: 3px solid #333333;
315
- border-radius: 50%;
316
- "></div>
317
-
318
- <!-- Logo -->
319
- <svg class="ww-loading-logo" width="80" height="50" viewBox="0 0 214 135" fill="none" xmlns="http://www.w3.org/2000/svg">
320
- <path d="M102.906 74.8679C102.906 77.9717 101.574 80.7453 98.9104 83.1887C96.6871 85.1918 93.9025 86.6997 90.5566 87.7123C87.695 88.5708 84.6462 89 81.4104 89C73.8821 89 68.0047 87.0189 63.7783 83.0566C59.5519 87.0189 53.6855 89 46.1792 89C42.9434 89 39.9057 88.5708 37.066 87.7123C33.7201 86.6997 30.9245 85.1918 28.6792 83.1887C26.0157 80.7453 24.684 77.9717 24.684 74.8679V41.6509H32.3774V74.8679C32.3774 76.2547 33.489 77.5645 35.7123 78.7972C37.3632 79.7217 39.0692 80.3711 40.8302 80.7453C42.5252 81.1195 44.3082 81.3066 46.1792 81.3066C48.0063 81.3066 49.7673 81.1195 51.4623 80.7453C53.2453 80.3711 54.9623 79.7217 56.6132 78.7972C58.8585 77.5645 59.9811 76.2547 59.9811 74.8679V41.6509H67.6085V74.8679C67.6085 76.2547 68.7311 77.5645 70.9764 78.7972C72.6274 79.7217 74.3443 80.3711 76.1274 80.7453C77.8223 81.1195 79.5833 81.3066 81.4104 81.3066C83.2814 81.3066 85.0755 81.1195 86.7925 80.7453C88.5314 80.3711 90.2264 79.7217 91.8774 78.7972C94.1006 77.5645 95.2123 76.2547 95.2123 74.8679V41.6509H102.906V74.8679ZM189.283 74.8679C189.283 77.9717 187.951 80.7453 185.288 83.1887C183.064 85.1918 180.28 86.6997 176.934 87.7123C174.072 88.5708 171.024 89 167.788 89C160.259 89 154.382 87.0189 150.156 83.0566C145.929 87.0189 140.063 89 132.557 89C129.321 89 126.283 88.5708 123.443 87.7123C120.097 86.6997 117.302 85.1918 115.057 83.1887C112.393 80.7453 111.061 77.9717 111.061 74.8679V41.6509H118.755V74.8679C118.755 76.2547 119.866 77.5645 122.09 78.7972C123.741 79.7217 125.447 80.3711 127.208 80.7453C128.903 81.1195 130.686 81.3066 132.557 81.3066C134.384 81.3066 136.145 81.1195 137.84 80.7453C139.623 80.3711 141.34 79.7217 142.991 78.7972C145.236 77.5645 146.358 76.2547 146.358 74.8679V41.6509H153.986V74.8679C153.986 76.2547 155.108 77.5645 157.354 78.7972C159.005 79.7217 160.722 80.3711 162.505 80.7453C164.2 81.1195 165.961 81.3066 167.788 81.3066C169.659 81.3066 171.453 81.1195 173.17 80.7453C174.909 80.3711 176.604 79.7217 178.255 78.7972C180.478 77.5645 181.59 76.2547 181.59 74.8679V41.6509H189.283V74.8679Z" fill="white"/>
321
- </svg>
322
- </div>
323
-
324
- <div style="font-size: 17px; font-weight: 500; text-align: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: white; letter-spacing: 0.5px; max-width: 280px;">${text}</div>
325
-
326
- <div style="display: flex; gap: 10px; margin-top: 4px;">
327
- <div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out infinite;"></div>
328
- <div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out 0.2s infinite;"></div>
329
- <div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out 0.4s infinite;"></div>
330
- </div>
331
- </div>
272
+ overlay.innerHTML = `
273
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 32px;">
274
+ <div style="position: relative; width: 120px; height: 120px; display: flex; align-items: center; justify-content: center;">
275
+ <!-- Outer spinning circle -->
276
+ <div class="ww-loading-spinner" style="
277
+ position: absolute;
278
+ width: 120px;
279
+ height: 120px;
280
+ border: 3px solid transparent;
281
+ border-top: 3px solid #333333;
282
+ border-right: 3px solid #333333;
283
+ border-radius: 50%;
284
+ "></div>
285
+
286
+ <!-- Logo -->
287
+ <svg class="ww-loading-logo" width="80" height="50" viewBox="0 0 214 135" fill="none" xmlns="http://www.w3.org/2000/svg">
288
+ <path d="M102.906 74.8679C102.906 77.9717 101.574 80.7453 98.9104 83.1887C96.6871 85.1918 93.9025 86.6997 90.5566 87.7123C87.695 88.5708 84.6462 89 81.4104 89C73.8821 89 68.0047 87.0189 63.7783 83.0566C59.5519 87.0189 53.6855 89 46.1792 89C42.9434 89 39.9057 88.5708 37.066 87.7123C33.7201 86.6997 30.9245 85.1918 28.6792 83.1887C26.0157 80.7453 24.684 77.9717 24.684 74.8679V41.6509H32.3774V74.8679C32.3774 76.2547 33.489 77.5645 35.7123 78.7972C37.3632 79.7217 39.0692 80.3711 40.8302 80.7453C42.5252 81.1195 44.3082 81.3066 46.1792 81.3066C48.0063 81.3066 49.7673 81.1195 51.4623 80.7453C53.2453 80.3711 54.9623 79.7217 56.6132 78.7972C58.8585 77.5645 59.9811 76.2547 59.9811 74.8679V41.6509H67.6085V74.8679C67.6085 76.2547 68.7311 77.5645 70.9764 78.7972C72.6274 79.7217 74.3443 80.3711 76.1274 80.7453C77.8223 81.1195 79.5833 81.3066 81.4104 81.3066C83.2814 81.3066 85.0755 81.1195 86.7925 80.7453C88.5314 80.3711 90.2264 79.7217 91.8774 78.7972C94.1006 77.5645 95.2123 76.2547 95.2123 74.8679V41.6509H102.906V74.8679ZM189.283 74.8679C189.283 77.9717 187.951 80.7453 185.288 83.1887C183.064 85.1918 180.28 86.6997 176.934 87.7123C174.072 88.5708 171.024 89 167.788 89C160.259 89 154.382 87.0189 150.156 83.0566C145.929 87.0189 140.063 89 132.557 89C129.321 89 126.283 88.5708 123.443 87.7123C120.097 86.6997 117.302 85.1918 115.057 83.1887C112.393 80.7453 111.061 77.9717 111.061 74.8679V41.6509H118.755V74.8679C118.755 76.2547 119.866 77.5645 122.09 78.7972C123.741 79.7217 125.447 80.3711 127.208 80.7453C128.903 81.1195 130.686 81.3066 132.557 81.3066C134.384 81.3066 136.145 81.1195 137.84 80.7453C139.623 80.3711 141.34 79.7217 142.991 78.7972C145.236 77.5645 146.358 76.2547 146.358 74.8679V41.6509H153.986V74.8679C153.986 76.2547 155.108 77.5645 157.354 78.7972C159.005 79.7217 160.722 80.3711 162.505 80.7453C164.2 81.1195 165.961 81.3066 167.788 81.3066C169.659 81.3066 171.453 81.1195 173.17 80.7453C174.909 80.3711 176.604 79.7217 178.255 78.7972C180.478 77.5645 181.59 76.2547 181.59 74.8679V41.6509H189.283V74.8679Z" fill="white"/>
289
+ </svg>
290
+ </div>
291
+
292
+ <div style="font-size: 17px; font-weight: 500; text-align: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: white; letter-spacing: 0.5px; max-width: 280px;">${text}</div>
293
+
294
+ <div style="display: flex; gap: 10px; margin-top: 4px;">
295
+ <div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out infinite;"></div>
296
+ <div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out 0.2s infinite;"></div>
297
+ <div style="width: 10px; height: 10px; border-radius: 50%; background: #333333; animation: ww-dot-bounce 1.4s ease-in-out 0.4s infinite;"></div>
298
+ </div>
299
+ </div>
332
300
  `;
333
301
  return overlay;
334
302
  }
335
- /**
336
- * Shows a loading overlay in the product gallery container
337
- */
338
303
  function showProductLoading(container, text = "Generating virtual try-on...") {
339
- // Remove any existing loading overlays
340
- removeProductLoading(container);
341
- // Make container relative if it's not already positioned
304
+ const existingOverlay = container.querySelector(".ww-loading-overlay");
305
+ if (existingOverlay) {
306
+ const textElement = existingOverlay.querySelector('div[style*="font-size: 17px"]');
307
+ if (textElement) {
308
+ textElement.textContent = text;
309
+ }
310
+ return;
311
+ }
342
312
  const computedStyle = window.getComputedStyle(container);
343
313
  if (computedStyle.position === "static") {
344
314
  container.style.position = "relative";
@@ -346,9 +316,6 @@
346
316
  const loadingOverlay = createLoadingOverlay(text);
347
317
  container.appendChild(loadingOverlay);
348
318
  }
349
- /**
350
- * Removes loading overlay from product container
351
- */
352
319
  function removeProductLoading(container) {
353
320
  const existingOverlay = container.querySelector(".ww-loading-overlay");
354
321
  if (existingOverlay) {
@@ -356,99 +323,74 @@
356
323
  }
357
324
  }
358
325
 
359
- function showAlert(container, message, type) {
360
- var _a;
361
- removeAlert(container); // Remove any existing alert
362
- const alertDiv = document.createElement("div");
363
- alertDiv.className = `ww-alert ww-alert-${type}`;
364
- alertDiv.setAttribute("role", "alert" );
365
- alertDiv.setAttribute("aria-live", "assertive");
366
- const bgColor = "linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)"
367
- ;
368
- const textColor = "#991b1b" ;
369
- const borderColor = "#f87171" ;
370
- const iconColor = "#dc2626" ;
371
- alertDiv.style.cssText = `
372
- position: absolute;
373
- top: 24px;
374
- left: 50%;
375
- transform: translateX(-50%) translateY(-20px);
376
- background: ${bgColor};
377
- color: ${textColor};
378
- border: 1px solid ${borderColor};
379
- border-radius: 12px;
380
- padding: 16px 20px;
381
- z-index: 9999;
382
- font-size: 15px;
383
- font-weight: 500;
384
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
385
- backdrop-filter: blur(8px);
386
- -webkit-backdrop-filter: blur(8px);
387
- display: flex;
388
- align-items: center;
389
- gap: 12px;
390
- min-width: 280px;
391
- max-width: 90%;
392
- opacity: 0;
393
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
394
- `;
395
- const icon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="${iconColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
396
- <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
397
- </svg>`
398
- ;
399
- alertDiv.innerHTML = `
400
- ${icon}
401
- <span style="flex: 1; line-height: 1.5;">${message}</span>
402
- <button type="button" aria-label="Close alert"
403
- style="
404
- background: none;
405
- border: none;
406
- font-size: 22px;
407
- color: ${textColor};
408
- cursor: pointer;
409
- padding: 0;
410
- line-height: 1;
411
- opacity: 0.7;
412
- transition: opacity 0.2s ease;
413
- width: 24px;
414
- height: 24px;
415
- display: flex;
416
- align-items: center;
417
- justify-content: center;
418
- "
419
- onmouseover="this.style.opacity='1'"
420
- onmouseout="this.style.opacity='0.7'">
421
- &times;
422
- </button>
423
- `;
424
- // Close on button click
425
- (_a = alertDiv.querySelector("button")) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => {
426
- fadeOutAndRemove(alertDiv);
427
- });
428
- container.appendChild(alertDiv);
429
- // Animate in
430
- requestAnimationFrame(() => {
431
- alertDiv.style.opacity = "1";
432
- alertDiv.style.transform = "translateX(-50%) translateY(0)";
433
- });
434
- // Auto-dismiss after 8 seconds
435
- setTimeout(() => {
436
- if (alertDiv.parentElement) {
437
- fadeOutAndRemove(alertDiv);
438
- }
439
- }, 8000);
326
+ function createStatusBadge(message) {
327
+ const badge = document.createElement("div");
328
+ badge.className = "ww-status-badge";
329
+ badge.style.cssText = `
330
+ position: absolute;
331
+ bottom: 16px;
332
+ left: 50%;
333
+ transform: translateX(-50%);
334
+ background: linear-gradient(135deg, rgba(0, 0, 0, 0.92) 0%, rgba(0, 0, 0, 0.88) 100%);
335
+ backdrop-filter: blur(12px);
336
+ -webkit-backdrop-filter: blur(12px);
337
+ color: white;
338
+ padding: 12px 24px;
339
+ border-radius: 24px;
340
+ font-size: 14px;
341
+ font-weight: 500;
342
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
343
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(0, 0, 0, 0.2);
344
+ z-index: 1000;
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 10px;
348
+ animation: ww-badge-fade-in 0.3s ease;
349
+ letter-spacing: 0.3px;
350
+ `;
351
+ if (!document.getElementById("ww-status-badge-animation")) {
352
+ const style = document.createElement("style");
353
+ style.id = "ww-status-badge-animation";
354
+ style.textContent = `
355
+ @keyframes ww-badge-fade-in {
356
+ from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
357
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
358
+ }
359
+
360
+ @keyframes ww-badge-pulse {
361
+ 0%, 100% { opacity: 0.6; }
362
+ 50% { opacity: 1; }
363
+ }
364
+ `;
365
+ document.head.appendChild(style);
366
+ }
367
+ badge.innerHTML = `
368
+ <div style="display: flex; gap: 4px;">
369
+ <div style="width: 6px; height: 6px; border-radius: 50%; background: white; animation: ww-badge-pulse 1.4s ease-in-out infinite;"></div>
370
+ <div style="width: 6px; height: 6px; border-radius: 50%; background: white; animation: ww-badge-pulse 1.4s ease-in-out 0.2s infinite;"></div>
371
+ <div style="width: 6px; height: 6px; border-radius: 50%; background: white; animation: ww-badge-pulse 1.4s ease-in-out 0.4s infinite;"></div>
372
+ </div>
373
+ <span>${message}</span>
374
+ `;
375
+ return badge;
440
376
  }
441
- function fadeOutAndRemove(element) {
442
- element.style.opacity = "0";
443
- element.style.transform = "translateX(-50%) translateY(-20px)";
444
- setTimeout(() => {
445
- element.remove();
446
- }, 300);
377
+ function showStatusBadge(container, message) {
378
+ const existingBadge = container.querySelector(".ww-status-badge");
379
+ if (existingBadge) {
380
+ const textElement = existingBadge.querySelector("span");
381
+ if (textElement) {
382
+ textElement.textContent = message;
383
+ }
384
+ return;
385
+ }
386
+ const badge = createStatusBadge(message);
387
+ container.appendChild(badge);
447
388
  }
448
- function removeAlert(container) {
449
- const alert = container.querySelector(".ww-alert");
450
- if (alert)
451
- fadeOutAndRemove(alert);
389
+ function removeStatusBadge(container) {
390
+ const badge = container.querySelector(".ww-status-badge");
391
+ if (badge) {
392
+ badge.remove();
393
+ }
452
394
  }
453
395
 
454
396
  class VirtualTryOnWidget {
@@ -460,7 +402,6 @@
460
402
  this.lastModelImage = null;
461
403
  this.cameraButton = null;
462
404
  this.iframeMessageListener = null;
463
- this.previewBadge = null;
464
405
  this.config = {
465
406
  baseUrl: config.baseUrl,
466
407
  productPageSelector: config.productPageSelector,
@@ -469,51 +410,36 @@
469
410
  buttonPosition: config.buttonPosition,
470
411
  };
471
412
  }
472
- /**
473
- * Initializes the virtual try-on widget on the current page
474
- * @returns Promise that resolves when initialization is complete
475
- */
476
413
  async init() {
477
414
  try {
478
- // Check if we're on a product page
479
415
  if (!window.location.pathname.includes(this.config.productPageSelector)) {
480
416
  return;
481
417
  }
482
- // Find the gallery container
483
418
  const container = document.querySelector(this.config.gallerySelector);
484
419
  if (!container || !(container instanceof HTMLElement)) {
485
420
  console.warn("[WeWear VTO] Gallery container not found:", this.config.gallerySelector);
486
421
  return;
487
422
  }
488
- // Ensure container has relative positioning for button placement
489
423
  if (getComputedStyle(container).position === "static") {
490
424
  container.style.position = "relative";
491
425
  }
492
- // Create and add the virtual try-on button
493
426
  const button = createButton(this.config.buttonPosition, async () => {
494
427
  if (this.cameraButton && !this.cameraButton.disabled) {
495
428
  await this.handleTryOnClick();
496
429
  }
497
430
  });
498
- // Store reference to camera button for dynamic enable/disable
499
431
  this.cameraButton = button.querySelector(".ww-camera-btn");
500
432
  container.appendChild(button);
501
433
  console.log("[WeWear VTO] Widget initialized successfully");
502
- // Listen for messages from the photo upload page
503
434
  this.setupIframeListener();
504
435
  }
505
436
  catch (error) {
506
437
  console.error("[WeWear VTO] Initialization failed:", error);
507
438
  }
508
439
  }
509
- /**
510
- * Handles virtual try-on button click
511
- * @private
512
- */
513
440
  async handleTryOnClick() {
514
441
  console.log("[WeWear VTO] Button clicked, starting try-on process...");
515
442
  try {
516
- // Store original images on first click if not already stored
517
443
  if (this.originalProductImages.length === 0) {
518
444
  this.originalProductImages = this.getAllProductImages();
519
445
  console.log("[WeWear VTO] Stored original product images:", this.originalProductImages);
@@ -525,13 +451,10 @@
525
451
  console.warn("[WeWear VTO] Product images not found:", this.config.productImageSelector);
526
452
  return;
527
453
  }
528
- // Store the first image as the original
529
454
  if (!this.originalProductImageUrl) {
530
455
  this.originalProductImageUrl = this.originalProductImages[0];
531
456
  }
532
- // Open the photo upload page in a modal
533
457
  const photoUploadUrl = new URL(`${this.config.baseUrl}`);
534
- // Pass all product images as a comma-separated string
535
458
  photoUploadUrl.searchParams.append("ww_product_images", this.originalProductImages.join(","));
536
459
  this.showPhotoUploadModal(photoUploadUrl.toString());
537
460
  }
@@ -539,11 +462,6 @@
539
462
  console.error("[WeWear VTO] Try-on request failed:", error);
540
463
  }
541
464
  }
542
- /**
543
- * Gets all product images from the gallery
544
- * @private
545
- * @returns Array of product image URLs
546
- */
547
465
  getAllProductImages() {
548
466
  const productImageElements = document.querySelectorAll(this.config.productImageSelector);
549
467
  const images = [];
@@ -552,7 +470,6 @@
552
470
  img.getAttribute("data-src") ||
553
471
  img.getAttribute("data-large_image") ||
554
472
  "";
555
- // Only include http/https URLs, exclude blob URLs and data URLs
556
473
  if (imageUrl &&
557
474
  (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) &&
558
475
  !images.includes(imageUrl)) {
@@ -561,92 +478,86 @@
561
478
  });
562
479
  return images;
563
480
  }
564
- /**
565
- * Shows a modal with an iframe for the photo upload page
566
- * @private
567
- */
568
481
  showPhotoUploadModal(url) {
569
- // Remove existing modals
570
482
  removeElements(`.${CSS_CLASSES.MODAL}`);
571
483
  const modal = document.createElement("div");
572
484
  modal.className = CSS_CLASSES.MODAL;
573
- modal.style.cssText = `
574
- position: fixed;
575
- top: 0;
576
- left: 0;
577
- right: 0;
578
- bottom: 0;
579
- width: 100%;
580
- height: 100%;
581
- background-color: rgba(0, 0, 0, 0.8);
582
- display: flex;
583
- justify-content: center;
584
- align-items: center;
585
- z-index: 999999999;
586
- animation: ww-modal-fade-in 0.3s ease;
485
+ modal.style.cssText = `
486
+ position: fixed;
487
+ top: 0;
488
+ left: 0;
489
+ right: 0;
490
+ bottom: 0;
491
+ width: 100%;
492
+ height: 100%;
493
+ background-color: rgba(0, 0, 0, 0.8);
494
+ display: flex;
495
+ justify-content: center;
496
+ align-items: center;
497
+ z-index: 999999999;
498
+ animation: ww-modal-fade-in 0.3s ease;
587
499
  `;
588
- // Add fade-in animation
589
500
  if (!document.getElementById("ww-modal-animation")) {
590
501
  const style = document.createElement("style");
591
502
  style.id = "ww-modal-animation";
592
- style.textContent = `
593
- @keyframes ww-modal-fade-in {
594
- from { opacity: 0; }
595
- to { opacity: 1; }
596
- }
597
- @keyframes ww-modal-scale-in {
598
- from { transform: scale(0.95); opacity: 0; }
599
- to { transform: scale(1); opacity: 1; }
600
- }
503
+ style.textContent = `
504
+ @keyframes ww-modal-fade-in {
505
+ from { opacity: 0; }
506
+ to { opacity: 1; }
507
+ }
508
+ @keyframes ww-modal-scale-in {
509
+ from { transform: scale(0.95); opacity: 0; }
510
+ to { transform: scale(1); opacity: 1; }
511
+ }
601
512
  `;
602
513
  document.head.appendChild(style);
603
514
  }
604
515
  const iframeContainer = document.createElement("div");
605
- iframeContainer.style.cssText = `
606
- position: relative;
607
- width: 90%;
608
- height: 90%;
609
- max-width: 480px;
610
- max-height: 720px;
611
- border-radius: 20px;
612
- overflow: hidden;
613
- box-shadow: 0 24px 48px rgba(0, 0, 0, 0.3), 0 12px 24px rgba(0, 0, 0, 0.2);
614
- animation: ww-modal-scale-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
615
- background: white;
516
+ iframeContainer.style.cssText = `
517
+ position: relative;
518
+ width: 90%;
519
+ height: 90%;
520
+ max-width: 480px;
521
+ max-height: 720px;
522
+ border-radius: 20px;
523
+ overflow: hidden;
524
+ box-shadow: 0 24px 48px rgba(0, 0, 0, 0.3), 0 12px 24px rgba(0, 0, 0, 0.2);
525
+ animation: ww-modal-scale-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
526
+ background: white;
616
527
  `;
617
528
  const iframe = document.createElement("iframe");
618
529
  iframe.src = url;
619
- iframe.style.cssText = `
620
- width: 100%;
621
- height: 100%;
622
- border: none;
530
+ iframe.style.cssText = `
531
+ width: 100%;
532
+ height: 100%;
533
+ border: none;
623
534
  `;
624
535
  const closeButton = document.createElement("button");
625
- closeButton.innerHTML = `
626
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
627
- <line x1="18" y1="6" x2="6" y2="18"></line>
628
- <line x1="6" y1="6" x2="18" y2="18"></line>
629
- </svg>
536
+ closeButton.innerHTML = `
537
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
538
+ <line x1="18" y1="6" x2="6" y2="18"></line>
539
+ <line x1="6" y1="6" x2="18" y2="18"></line>
540
+ </svg>
630
541
  `;
631
542
  closeButton.setAttribute("aria-label", "Close modal");
632
- closeButton.style.cssText = `
633
- position: absolute;
634
- top: -48px;
635
- right: 0;
636
- background: rgba(255, 255, 255, 0.95);
637
- backdrop-filter: blur(8px);
638
- -webkit-backdrop-filter: blur(8px);
639
- border: none;
640
- border-radius: 50%;
641
- width: 40px;
642
- height: 40px;
643
- cursor: pointer;
644
- display: flex;
645
- align-items: center;
646
- justify-content: center;
647
- color: #1f2937;
648
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
649
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
543
+ closeButton.style.cssText = `
544
+ position: absolute;
545
+ top: -48px;
546
+ right: 0;
547
+ background: rgba(255, 255, 255, 0.95);
548
+ backdrop-filter: blur(8px);
549
+ -webkit-backdrop-filter: blur(8px);
550
+ border: none;
551
+ border-radius: 50%;
552
+ width: 40px;
553
+ height: 40px;
554
+ cursor: pointer;
555
+ display: flex;
556
+ align-items: center;
557
+ justify-content: center;
558
+ color: #1f2937;
559
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
560
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
650
561
  `;
651
562
  closeButton.onmouseover = () => {
652
563
  closeButton.style.transform = "scale(1.1) rotate(90deg)";
@@ -668,19 +579,13 @@
668
579
  iframeContainer.appendChild(closeButton);
669
580
  modal.appendChild(iframeContainer);
670
581
  document.body.appendChild(modal);
671
- // Close on backdrop click
672
582
  modal.onclick = (e) => {
673
583
  if (e.target === modal) {
674
584
  closeButton.click();
675
585
  }
676
586
  };
677
587
  }
678
- /**
679
- * Sets up listener for messages from the iframe
680
- * @private
681
- */
682
588
  setupIframeListener() {
683
- // Remove existing listener if any
684
589
  if (this.iframeMessageListener) {
685
590
  window.removeEventListener("message", this.iframeMessageListener);
686
591
  }
@@ -710,7 +615,6 @@
710
615
  console.warn("[WeWear VTO] Missing required data to start virtual try-on.");
711
616
  return;
712
617
  }
713
- // Use stored original images, or get them if not stored yet
714
618
  if (this.originalProductImages.length === 0) {
715
619
  this.originalProductImages = this.getAllProductImages();
716
620
  }
@@ -718,37 +622,56 @@
718
622
  console.warn("[WeWear VTO] No product images found.");
719
623
  return;
720
624
  }
721
- // Hide the modal and show loading indicator
722
625
  const modal = document.querySelector(`.${CSS_CLASSES.MODAL}`);
723
626
  if (modal && modal instanceof HTMLElement) {
724
627
  modal.style.display = "none";
725
628
  }
726
629
  const container = document.querySelector(this.config.gallerySelector);
727
630
  if (container instanceof HTMLElement) {
728
- showProductLoading(container, "Generating your virtual try-on...");
631
+ showProductLoading(container, "Preparing your personalized look");
729
632
  }
633
+ let hasPreview = false;
730
634
  try {
731
635
  const submitResponse = await this.callVtoApi(this.lastModelImage, this.originalProductImages);
732
636
  let status = await this.fetchJobStatus(submitResponse.job_id);
733
637
  let lastPreviewCount = 0;
734
638
  while (status.status !== "COMPLETED" && status.status !== "FAILED") {
735
- // Check if a new preview is available
639
+ if (!hasPreview && container instanceof HTMLElement) {
640
+ let statusMessage = "Preparing your personalized look";
641
+ if (status.current_iteration === 1) {
642
+ statusMessage = "Refining your look";
643
+ }
644
+ else if (status.current_iteration && status.current_iteration > 1) {
645
+ statusMessage = "Finalizing your look";
646
+ }
647
+ showProductLoading(container, statusMessage);
648
+ }
649
+ else if (hasPreview && container instanceof HTMLElement) {
650
+ let statusMessage = "Refining your look";
651
+ if (status.current_iteration && status.current_iteration > 1) {
652
+ statusMessage = "Finalizing your look";
653
+ }
654
+ showStatusBadge(container, statusMessage);
655
+ this.hideButtonContainer(container);
656
+ }
736
657
  if (status.preview_count > lastPreviewCount) {
737
658
  try {
738
659
  const previewUrl = await this.fetchJobImage(submitResponse.job_id);
739
- this.replaceProductImage(previewUrl);
740
- lastPreviewCount = status.preview_count;
741
- // Add badge for each preview if not already present
742
- if (container instanceof HTMLElement) {
743
- if (!this.previewBadge) {
744
- this.previewBadge = createPreviewBadge();
745
- container.appendChild(this.previewBadge);
746
- }
747
- else if (!container.contains(this.previewBadge)) {
748
- // Re-add badge if it was removed
749
- container.appendChild(this.previewBadge);
660
+ if (!hasPreview && container instanceof HTMLElement) {
661
+ this.replaceProductImage(previewUrl);
662
+ let statusMessage = "Refining your look";
663
+ if (status.current_iteration && status.current_iteration > 1) {
664
+ statusMessage = "Finalizing your look";
750
665
  }
666
+ showStatusBadge(container, statusMessage);
667
+ this.hideButtonContainer(container);
668
+ removeProductLoading(container);
669
+ hasPreview = true;
751
670
  }
671
+ else if (hasPreview) {
672
+ this.replaceProductImage(previewUrl);
673
+ }
674
+ lastPreviewCount = status.preview_count;
752
675
  }
753
676
  catch (e) {
754
677
  if (!(e instanceof Error && e.message === "202_PROCESSING"))
@@ -763,27 +686,41 @@
763
686
  if (finalImage) {
764
687
  this.replaceProductImage(finalImage);
765
688
  }
766
- if (this.previewBadge) {
767
- this.previewBadge.remove();
768
- this.previewBadge = null;
689
+ if (container instanceof HTMLElement) {
690
+ removeStatusBadge(container);
691
+ this.showButtonContainer(container);
769
692
  }
770
693
  }
771
694
  if (status.status === "FAILED") {
772
695
  console.error("[WeWear VTO] VTO process failed:", status.message);
773
696
  if (container instanceof HTMLElement) {
774
- showAlert(container, status.message, "error");
697
+ if (!hasPreview) {
698
+ showProductLoading(container, "We are experiencing some technical issues and apologise for any inconvenience caused. Please come back later again");
699
+ }
700
+ else {
701
+ showStatusBadge(container, "We are experiencing some technical issues and apologise for any inconvenience caused. Please come back later again");
702
+ }
775
703
  }
704
+ await new Promise((r) => setTimeout(r, 5000));
776
705
  }
777
706
  }
778
707
  catch (error) {
779
708
  console.error("[WeWear VTO] Error during virtual try-on process:", error);
780
709
  if (container instanceof HTMLElement) {
781
- showAlert(container, "An unexpected error occurred.", "error");
710
+ if (!hasPreview) {
711
+ showProductLoading(container, "We are experiencing technical issues and apologise for any inconvenience caused. Please come back later again");
712
+ }
713
+ else {
714
+ showStatusBadge(container, "We are experiencing technical issues. Please try again later.");
715
+ }
782
716
  }
717
+ await new Promise((r) => setTimeout(r, 5000));
783
718
  }
784
719
  finally {
785
720
  if (container instanceof HTMLElement) {
786
721
  removeProductLoading(container);
722
+ removeStatusBadge(container);
723
+ this.showButtonContainer(container);
787
724
  }
788
725
  removeElements(`.${CSS_CLASSES.MODAL}`);
789
726
  }
@@ -791,7 +728,6 @@
791
728
  async callVtoApi(modelImage, productImages) {
792
729
  const formData = new FormData();
793
730
  formData.append("model_image", modelImage, "model_image.png");
794
- // Send all product images as a JSON array
795
731
  formData.append("product_image_urls", JSON.stringify(productImages));
796
732
  const res = await fetch(`${this.config.baseUrl}/api/vto`, {
797
733
  method: "POST",
@@ -823,38 +759,27 @@
823
759
  async refreshVirtualTryOn() {
824
760
  await this.startVirtualTryOn(true);
825
761
  }
826
- /**
827
- * Replaces the product image in the gallery with the virtual try-on result
828
- * @private
829
- */
830
762
  replaceProductImage(imageUrl) {
831
763
  try {
832
- // Store the virtual try-on image URL
833
764
  this.virtualTryOnImageUrl = imageUrl;
834
- // Find the gallery container
835
765
  const container = document.querySelector(this.config.gallerySelector);
836
766
  if (!container || !(container instanceof HTMLElement)) {
837
767
  console.warn("[WeWear VTO] Gallery container not found for image replacement:", this.config.gallerySelector);
838
768
  return;
839
769
  }
840
- // Store the original content if not already stored
841
770
  if (!container.hasAttribute("data-ww-original-content")) {
842
771
  container.setAttribute("data-ww-original-content", container.innerHTML);
843
772
  }
844
- // Capture and lock container dimensions before replacement to prevent layout shift
845
773
  if (!container.hasAttribute("data-ww-container-locked")) {
846
774
  const containerRect = container.getBoundingClientRect();
847
- // Store original dimensions
848
775
  container.setAttribute("data-ww-container-width", containerRect.width.toString());
849
776
  container.setAttribute("data-ww-container-height", containerRect.height.toString());
850
- // Lock the container dimensions
851
777
  container.style.width = `${containerRect.width}px`;
852
778
  container.style.height = `${containerRect.height}px`;
853
779
  container.style.minWidth = `${containerRect.width}px`;
854
780
  container.style.minHeight = `${containerRect.height}px`;
855
781
  container.setAttribute("data-ww-container-locked", "true");
856
782
  }
857
- // Capture original image dimensions before replacement to prevent layout shift
858
783
  const originalImg = container.querySelector("img");
859
784
  if (originalImg &&
860
785
  !container.hasAttribute("data-ww-original-dimensions")) {
@@ -866,9 +791,7 @@
866
791
  container.setAttribute("data-ww-original-rect-height", rect.height.toString());
867
792
  container.setAttribute("data-ww-original-dimensions", "true");
868
793
  }
869
- // Show the virtual try-on image initially
870
794
  this.showVirtualTryOnImage(container);
871
- // Replace the button container with the new multi-button version
872
795
  this.updateButtonContainer(container);
873
796
  console.log("[WeWear VTO] Product image replaced with virtual try-on result");
874
797
  }
@@ -876,17 +799,23 @@
876
799
  console.error("[WeWear VTO] Error replacing product image:", error);
877
800
  }
878
801
  }
879
- /**
880
- * Updates the button container to show all available buttons
881
- * @private
882
- */
802
+ hideButtonContainer(container) {
803
+ const buttonContainer = container.querySelector(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
804
+ if (buttonContainer) {
805
+ buttonContainer.style.display = "none";
806
+ }
807
+ }
808
+ showButtonContainer(container) {
809
+ const buttonContainer = container.querySelector(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
810
+ if (buttonContainer) {
811
+ buttonContainer.style.display = "flex";
812
+ }
813
+ }
883
814
  updateButtonContainer(container) {
884
- // Remove existing button containers
885
815
  const existingButtons = container.querySelectorAll(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
886
816
  existingButtons.forEach((btn) => {
887
817
  btn.remove();
888
818
  });
889
- // Create new button container with all buttons
890
819
  const buttonContainer = createButtonContainer(this.config.buttonPosition, this.virtualTryOnImageUrl !== null, async () => {
891
820
  await this.handleTryOnClick();
892
821
  }, async () => {
@@ -903,22 +832,16 @@
903
832
  }, this.isShowingVirtualTryOn);
904
833
  container.appendChild(buttonContainer);
905
834
  }
906
- /**
907
- * Shows the virtual try-on image in the container
908
- * @private
909
- */
910
835
  showVirtualTryOnImage(container) {
911
836
  if (!this.virtualTryOnImageUrl) {
912
837
  console.warn("[WeWear VTO] No virtual try-on image URL available");
913
838
  return;
914
839
  }
915
- // Remove all direct children except for the button container
916
840
  Array.from(container.children).forEach((child) => {
917
841
  if (!child.classList.contains(CSS_CLASSES.BUTTON_CONTAINER)) {
918
842
  child.remove();
919
843
  }
920
844
  });
921
- // Get stored original dimensions to prevent layout shift
922
845
  const originalWidth = container.getAttribute("data-ww-original-width") || "";
923
846
  const originalHeight = container.getAttribute("data-ww-original-height") || "";
924
847
  const originalRectWidth = container.getAttribute("data-ww-original-rect-width") || "";
@@ -926,10 +849,8 @@
926
849
  const image = document.createElement("img");
927
850
  image.src = this.virtualTryOnImageUrl;
928
851
  image.alt = "Virtual Try-On Result";
929
- // Use original dimensions to prevent layout shift
930
852
  let widthStyle = "100%";
931
853
  let heightStyle = "100%";
932
- // Prefer computed style dimensions, fallback to bounding rect, then container fill
933
854
  if (originalWidth && originalWidth !== "auto" && originalWidth !== "0px") {
934
855
  widthStyle = originalWidth;
935
856
  }
@@ -944,61 +865,42 @@
944
865
  else if (originalRectHeight && originalRectHeight !== "0") {
945
866
  heightStyle = `${originalRectHeight}px`;
946
867
  }
947
- image.style.cssText = `
948
- width: ${widthStyle};
949
- height: ${heightStyle};
950
- object-fit: contain;
868
+ image.style.cssText = `
869
+ width: ${widthStyle};
870
+ height: ${heightStyle};
871
+ object-fit: contain;
951
872
  `;
952
- // Prepend the image to ensure buttons are rendered on top
953
873
  container.prepend(image);
954
874
  this.isShowingVirtualTryOn = true;
955
875
  }
956
- /**
957
- * Shows the original product image in the container
958
- * @private
959
- */
960
876
  showOriginalImage(container) {
961
877
  const originalContentHTML = container.getAttribute("data-ww-original-content");
962
878
  if (originalContentHTML) {
963
- // Remove all direct children except for the button container
964
879
  Array.from(container.children).forEach((child) => {
965
880
  if (!child.classList.contains(CSS_CLASSES.BUTTON_CONTAINER)) {
966
881
  child.remove();
967
882
  }
968
883
  });
969
- // Parse the original content and prepend it
970
884
  const tempDiv = document.createElement("div");
971
885
  tempDiv.innerHTML = originalContentHTML;
972
886
  container.prepend(...Array.from(tempDiv.children));
973
887
  }
974
888
  this.isShowingVirtualTryOn = false;
975
889
  }
976
- /**
977
- * Destroys the widget and cleans up resources
978
- */
979
890
  destroy() {
980
891
  try {
981
- // Remove all buttons
982
892
  removeElements(`.${CSS_CLASSES.BUTTON_CONTAINER}`);
983
- // Remove all modals
984
893
  removeElements(`.${CSS_CLASSES.MODAL}`);
985
- // Clear references
986
894
  this.cameraButton = null;
987
- // Remove message listener
988
895
  if (this.iframeMessageListener) {
989
896
  window.removeEventListener("message", this.iframeMessageListener);
990
897
  this.iframeMessageListener = null;
991
898
  }
992
- // Reset state
993
899
  this.virtualTryOnImageUrl = null;
994
900
  this.isShowingVirtualTryOn = false;
995
901
  this.lastModelImage = null;
996
902
  this.originalProductImageUrl = null;
997
903
  this.originalProductImages = [];
998
- if (this.previewBadge) {
999
- this.previewBadge.remove();
1000
- this.previewBadge = null;
1001
- }
1002
904
  console.log("[WeWear VTO] Widget destroyed successfully");
1003
905
  }
1004
906
  catch (error) {
@@ -1010,19 +912,15 @@
1010
912
  let widgetInstance = null;
1011
913
  function initVirtualTryOn(config) {
1012
914
  try {
1013
- // Clean up any existing instance
1014
915
  if (widgetInstance) {
1015
916
  widgetInstance.destroy();
1016
917
  widgetInstance = null;
1017
918
  }
1018
- // Early return if config is missing
1019
919
  if (!config) {
1020
920
  console.log("[WeWear VTO] Missing configuration. Widget not initialized.");
1021
921
  return;
1022
922
  }
1023
- // Create and initialize new instance
1024
923
  widgetInstance = new VirtualTryOnWidget(config);
1025
- // Initialize immediately if DOM is ready, otherwise wait for it
1026
924
  if (document.readyState === "loading") {
1027
925
  document.addEventListener("DOMContentLoaded", () => {
1028
926
  widgetInstance === null || widgetInstance === void 0 ? void 0 : widgetInstance.init().catch((error) => {