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