@primestyleai/tryon 3.0.0 → 3.1.1

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.
Files changed (2) hide show
  1. package/dist/react/index.js +123 -35
  2. package/package.json +1 -1
@@ -187,11 +187,9 @@ function PrimeStyleTryonInner({
187
187
  const [resultImageUrl, setResultImageUrl] = useState(null);
188
188
  const [errorMessage, setErrorMessage] = useState(null);
189
189
  const [dragOver, setDragOver] = useState(false);
190
- const countdownRef = useRef(25);
191
- const countdownElRef = useRef(null);
192
- const countdownCircleRef = useRef(null);
193
190
  const [sizingMethod, setSizingMethod] = useState(null);
194
191
  const [sizingResult, setSizingResult] = useState(null);
192
+ const [sizingLoading, setSizingLoading] = useState(false);
195
193
  const [sizeGuide, setSizeGuide] = useState(null);
196
194
  const [sizeGuideFetching, setSizeGuideFetching] = useState(false);
197
195
  const sizeGuideFetchedRef = useRef(false);
@@ -202,6 +200,7 @@ function PrimeStyleTryonInner({
202
200
  const [weightUnit, setWeightUnit] = useState(imperial ? "lbs" : "kg");
203
201
  const formRef = useRef({});
204
202
  const [formGender, setFormGender] = useState("male");
203
+ const [formKey, setFormKey] = useState(0);
205
204
  const [profiles, setProfiles] = useState(() => lsGet("profiles", []));
206
205
  const [history, setHistory] = useState(() => lsGet("history", []));
207
206
  const [activeProfileId, setActiveProfileId] = useState(null);
@@ -240,15 +239,33 @@ function PrimeStyleTryonInner({
240
239
  if (pollingRef.current) clearInterval(pollingRef.current);
241
240
  };
242
241
  }, [apiUrl]);
242
+ const progressRef = useRef(0);
243
+ const progressBarRef = useRef(null);
244
+ const progressTextRef = useRef(null);
245
+ const progressStatusRef = useRef(null);
243
246
  useEffect(() => {
244
247
  if (view === "processing") {
245
- countdownRef.current = 25;
248
+ progressRef.current = 0;
249
+ const statuses = [
250
+ { at: 0, text: "Preparing your image..." },
251
+ { at: 15, text: "Analyzing body proportions..." },
252
+ { at: 30, text: "Matching garment to your photo..." },
253
+ { at: 50, text: "Generating virtual try-on..." },
254
+ { at: 75, text: "Refining details..." },
255
+ { at: 90, text: "Almost there..." }
256
+ ];
246
257
  const interval = setInterval(() => {
247
- countdownRef.current -= 1;
248
- if (countdownElRef.current) countdownElRef.current.textContent = String(Math.max(countdownRef.current, 0));
249
- if (countdownCircleRef.current) countdownCircleRef.current.style.strokeDashoffset = `${Math.max(countdownRef.current, 0) / 25 * 326.73}`;
250
- if (countdownRef.current <= 0) clearInterval(interval);
251
- }, 1e3);
258
+ const p = progressRef.current;
259
+ const increment = p < 30 ? 1.2 : p < 60 ? 0.8 : p < 80 ? 0.4 : p < 95 ? 0.15 : 0;
260
+ progressRef.current = Math.min(p + increment, 95);
261
+ const val = Math.round(progressRef.current);
262
+ if (progressBarRef.current) progressBarRef.current.style.width = `${val}%`;
263
+ if (progressTextRef.current) progressTextRef.current.textContent = `${val}%`;
264
+ if (progressStatusRef.current) {
265
+ const status = [...statuses].reverse().find((s) => val >= s.at);
266
+ if (status) progressStatusRef.current.textContent = status.text;
267
+ }
268
+ }, 200);
252
269
  return () => clearInterval(interval);
253
270
  }
254
271
  }, [view]);
@@ -332,6 +349,7 @@ function PrimeStyleTryonInner({
332
349
  setErrorMessage(null);
333
350
  setSizingMethod(null);
334
351
  setSizingResult(null);
352
+ setSizingLoading(false);
335
353
  setSizeGuide(null);
336
354
  setProfileSaved(false);
337
355
  formRef.current = {};
@@ -385,8 +403,12 @@ function PrimeStyleTryonInner({
385
403
  });
386
404
  if (!completedRef.current) {
387
405
  completedRef.current = true;
406
+ progressRef.current = 100;
407
+ if (progressBarRef.current) progressBarRef.current.style.width = "100%";
408
+ if (progressTextRef.current) progressTextRef.current.textContent = "100%";
409
+ if (progressStatusRef.current) progressStatusRef.current.textContent = "Complete!";
388
410
  cleanupJob();
389
- setView("result");
411
+ setTimeout(() => setView("result"), 400);
390
412
  onComplete?.({ jobId: update.galleryId, imageUrl: update.imageUrl });
391
413
  }
392
414
  } else if (update.status === "failed") {
@@ -439,8 +461,13 @@ function PrimeStyleTryonInner({
439
461
  if (res.ok) {
440
462
  const data = await res.json();
441
463
  setSizingResult(data);
464
+ } else {
465
+ console.warn("[PrimeStyle] Sizing API error:", res.status, await res.text().catch(() => ""));
442
466
  }
443
- } catch {
467
+ } catch (err) {
468
+ console.warn("[PrimeStyle] Sizing request failed:", err);
469
+ } finally {
470
+ setSizingLoading(false);
444
471
  }
445
472
  }, [apiUrl, sizingMethod, sizingCountry, heightUnit, weightUnit, sizingUnit, sizeGuide, productTitle]);
446
473
  const handleSubmit = useCallback(async () => {
@@ -453,7 +480,10 @@ function PrimeStyleTryonInner({
453
480
  }
454
481
  completedRef.current = false;
455
482
  setView("processing");
456
- if (sizingMethod) submitSizing();
483
+ if (sizingMethod) {
484
+ setSizingLoading(true);
485
+ submitSizing();
486
+ }
457
487
  try {
458
488
  const modelImage = await compressImage(selectedFile);
459
489
  const response = await apiRef.current.submitTryOn(modelImage, productImage);
@@ -516,6 +546,7 @@ function PrimeStyleTryonInner({
516
546
  setErrorMessage(null);
517
547
  setSizingMethod(null);
518
548
  setSizingResult(null);
549
+ setSizingLoading(false);
519
550
  setProfileSaved(false);
520
551
  setView("upload");
521
552
  }, [previewUrl, cleanupJob]);
@@ -541,6 +572,11 @@ function PrimeStyleTryonInner({
541
572
  if (p.fitPreference) fd.fitPreference = p.fitPreference;
542
573
  formRef.current = fd;
543
574
  setFormGender(fd.gender || "male");
575
+ if (p.country) setSizingCountry(p.country);
576
+ if (p.sizingUnit) setSizingUnit(p.sizingUnit);
577
+ if (p.heightUnit) setHeightUnit(p.heightUnit);
578
+ if (p.weightUnit) setWeightUnit(p.weightUnit);
579
+ setFormKey((k) => k + 1);
544
580
  }, [profiles]);
545
581
  const saveProfile = useCallback((name) => {
546
582
  const id = activeProfileId || `p_${Date.now()}`;
@@ -563,6 +599,10 @@ function PrimeStyleTryonInner({
563
599
  shoeUS: formRef.current.shoeUS,
564
600
  shoeUK: formRef.current.shoeUK,
565
601
  fitPreference: formRef.current.fitPreference,
602
+ country: sizingCountry,
603
+ sizingUnit,
604
+ heightUnit,
605
+ weightUnit,
566
606
  createdAt: Date.now()
567
607
  };
568
608
  setProfiles((prev) => {
@@ -576,7 +616,7 @@ function PrimeStyleTryonInner({
576
616
  });
577
617
  setActiveProfileId(id);
578
618
  setProfileSaved(true);
579
- }, [activeProfileId]);
619
+ }, [activeProfileId, sizingCountry, sizingUnit, heightUnit, weightUnit]);
580
620
  const saveHistoryEntry = useCallback(() => {
581
621
  const entry = {
582
622
  id: `h_${Date.now()}`,
@@ -827,7 +867,11 @@ function PrimeStyleTryonInner({
827
867
  /* @__PURE__ */ jsx("label", { children: "Sizing region" }),
828
868
  /* @__PURE__ */ jsx("select", { className: "ps-tryon-country-select", value: sizingCountry, onChange: (e) => setSizingCountry(e.target.value), children: SIZING_COUNTRIES.map((c) => /* @__PURE__ */ jsx("option", { value: c.code, children: c.label }, c.code)) })
829
869
  ] }),
830
- sizingMethod === "exact" && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "cm", value: "cm" }, { label: "in", value: "in" }], value: sizingUnit, onChange: setSizingUnit }) }),
870
+ sizingMethod === "exact" && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "cm", value: "cm" }, { label: "in", value: "in" }], value: sizingUnit, onChange: (v) => {
871
+ setSizingUnit(v);
872
+ setHeightUnit(v === "cm" ? "cm" : "ft");
873
+ setWeightUnit(v === "cm" ? "kg" : "lbs");
874
+ } }) }),
831
875
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
832
876
  /* @__PURE__ */ jsx("label", { children: "Height" }),
833
877
  heightUnit === "ft" ? /* @__PURE__ */ jsxs("div", { className: "ps-tryon-height-ft", children: [
@@ -883,27 +927,34 @@ function PrimeStyleTryonInner({
883
927
  "Get My Size & Try On ",
884
928
  /* @__PURE__ */ jsx(ArrowRightIcon, {})
885
929
  ] })
886
- ] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}`);
930
+ ] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}-${formKey}`);
887
931
  }
888
932
  function ProcessingView() {
889
933
  return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
890
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-countdown-ring", children: [
891
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 120 120", children: [
892
- /* @__PURE__ */ jsx("circle", { className: "ps-tryon-countdown-track", cx: "60", cy: "60", r: "52" }),
893
- /* @__PURE__ */ jsx("circle", { ref: countdownCircleRef, className: "ps-tryon-countdown-progress", cx: "60", cy: "60", r: "52", style: { strokeDashoffset: `${25 / 25 * 326.73}` } })
894
- ] }),
895
- /* @__PURE__ */ jsx("span", { ref: countdownElRef, className: "ps-tryon-countdown-number", children: "25" })
934
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing-image-wrap", children: [
935
+ previewUrl && /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "Your photo", className: "ps-tryon-processing-model" }),
936
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-line" }),
937
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-overlay" })
938
+ ] }),
939
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-section", children: [
940
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-progress-bar-wrap", children: /* @__PURE__ */ jsx("div", { ref: progressBarRef, className: "ps-tryon-progress-bar-fill", style: { width: "0%" } }) }),
941
+ /* @__PURE__ */ jsx("span", { ref: progressTextRef, className: "ps-tryon-progress-pct", children: "0%" })
896
942
  ] }),
897
- /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-text", cn.processingText), children: "Generating your try-on..." }),
943
+ /* @__PURE__ */ jsx("div", { ref: progressStatusRef, className: cx("ps-tryon-processing-text", cn.processingText), children: "Preparing your image..." }),
898
944
  /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-sub", cn.processingSubText), children: "This usually takes 15-25 seconds" })
899
945
  ] });
900
946
  }
901
947
  function ResultView() {
902
- const hasBoth = !!resultImageUrl && !!sizingResult;
948
+ const hasSizing = !!sizingResult || sizingLoading;
949
+ const hasBoth = !!resultImageUrl && hasSizing;
903
950
  const [profileName, setProfileName] = useState("");
904
951
  return /* @__PURE__ */ jsxs("div", { className: `ps-tryon-result-layout${hasBoth ? " ps-tryon-result-split" : ""}`, children: [
905
952
  resultImageUrl && /* @__PURE__ */ jsx("div", { className: "ps-tryon-result-image-col", children: /* @__PURE__ */ jsx("img", { src: resultImageUrl, alt: "Try-on result", className: cn.resultImage }) }),
906
953
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-result-info-col", children: [
954
+ sizingLoading && !sizingResult && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-recommend ps-tryon-sizing-loading", children: [
955
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-loading-spinner" }),
956
+ /* @__PURE__ */ jsx("p", { className: "ps-tryon-size-reasoning", children: "Analyzing your size..." })
957
+ ] }),
907
958
  sizingResult && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-recommend", children: [
908
959
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-badge", children: sizingResult.recommendedSize }),
909
960
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-confidence", children: [
@@ -1324,19 +1375,49 @@ const STYLES = `
1324
1375
  .ps-tryon-disclaimer { font-size: 11px; color: #666; margin: 4px 0 0; }
1325
1376
 
1326
1377
  /* Processing */
1327
- .ps-tryon-processing { text-align: center; padding: 40px 24px; }
1328
- .ps-tryon-spinner {
1329
- width: 48px; height: 48px; border: 3px solid #333;
1330
- border-top-color: var(--ps-loader, #bb945c); border-radius: 50%;
1331
- animation: ps-spin 0.8s linear infinite; margin: 0 auto 16px;
1378
+ .ps-tryon-processing { text-align: center; padding: 24px; display: flex; flex-direction: column; align-items: center; }
1379
+
1380
+ .ps-tryon-processing-image-wrap {
1381
+ position: relative; width: 200px; height: 260px; margin: 0 auto 24px;
1382
+ border-radius: 16px; overflow: hidden; border: 2px solid #333;
1332
1383
  }
1333
- @keyframes ps-spin { to { transform: rotate(360deg); } }
1334
- .ps-tryon-countdown-ring { position: relative; width: 100px; height: 100px; margin: 0 auto 20px; }
1335
- .ps-tryon-countdown-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
1336
- .ps-tryon-countdown-track { fill: none; stroke: #333; stroke-width: 4; }
1337
- .ps-tryon-countdown-progress { fill: none; stroke: var(--ps-loader, #bb945c); stroke-width: 4; stroke-linecap: round; stroke-dasharray: 326.73; transition: stroke-dashoffset 1s linear; }
1338
- .ps-tryon-countdown-number { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 28px; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums; }
1339
- .ps-tryon-done-pulse { animation: ps-scale-in 0.4s ease both; }
1384
+ .ps-tryon-processing-model {
1385
+ width: 100%; height: 100%; object-fit: cover; display: block;
1386
+ }
1387
+ .ps-tryon-scan-overlay {
1388
+ position: absolute; inset: 0;
1389
+ background: linear-gradient(180deg, rgba(187,148,92,0.05) 0%, transparent 40%, transparent 60%, rgba(187,148,92,0.05) 100%);
1390
+ pointer-events: none;
1391
+ }
1392
+ .ps-tryon-scan-line {
1393
+ position: absolute; left: 0; right: 0; height: 3px;
1394
+ background: linear-gradient(90deg, transparent, #bb945c 20%, #d6ba7d 50%, #bb945c 80%, transparent);
1395
+ box-shadow: 0 0 15px rgba(187,148,92,0.6), 0 0 40px rgba(187,148,92,0.2);
1396
+ animation: ps-scan 2.5s ease-in-out infinite;
1397
+ pointer-events: none; z-index: 2;
1398
+ }
1399
+ @keyframes ps-scan {
1400
+ 0% { top: 0; opacity: 0; }
1401
+ 5% { opacity: 1; }
1402
+ 95% { opacity: 1; }
1403
+ 100% { top: calc(100% - 3px); opacity: 0; }
1404
+ }
1405
+
1406
+ .ps-tryon-progress-section {
1407
+ display: flex; align-items: center; gap: 12px; width: 100%; max-width: 300px; margin-bottom: 16px;
1408
+ }
1409
+ .ps-tryon-progress-bar-wrap {
1410
+ flex: 1; height: 6px; background: #333; border-radius: 3px; overflow: hidden;
1411
+ }
1412
+ .ps-tryon-progress-bar-fill {
1413
+ height: 100%; background: linear-gradient(90deg, #bb945c, #d6ba7d);
1414
+ border-radius: 3px; transition: width 0.3s ease;
1415
+ }
1416
+ .ps-tryon-progress-pct {
1417
+ font-size: 13px; font-weight: 700; color: #bb945c; min-width: 36px; text-align: right;
1418
+ font-variant-numeric: tabular-nums;
1419
+ }
1420
+
1340
1421
  @keyframes ps-scale-in { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } }
1341
1422
  .ps-tryon-processing-text { font-size: 14px; color: #fff; margin: 0 0 4px; }
1342
1423
  .ps-tryon-processing-sub { font-size: 12px; color: #999; margin: 0; }
@@ -1366,6 +1447,13 @@ const STYLES = `
1366
1447
  background: linear-gradient(135deg, #bb945c, #d6ba7d);
1367
1448
  color: #111; font-size: 20px; font-weight: 700; margin-bottom: 10px;
1368
1449
  }
1450
+ .ps-tryon-sizing-loading { text-align: center; padding: 20px 0; }
1451
+ .ps-tryon-size-loading-spinner {
1452
+ width: 36px; height: 36px; border: 3px solid #333;
1453
+ border-top-color: #bb945c; border-radius: 50%;
1454
+ animation: ps-spin 0.8s linear infinite; margin: 0 auto 12px;
1455
+ }
1456
+ @keyframes ps-spin { to { transform: rotate(360deg); } }
1369
1457
  .ps-tryon-size-confidence { font-size: 12px; color: #999; margin-bottom: 8px; }
1370
1458
  .ps-conf-high { color: #4ade80; } .ps-conf-medium { color: #bb945c; } .ps-conf-low { color: #ef4444; }
1371
1459
  .ps-tryon-size-reasoning { font-size: 13px; color: #ccc; line-height: 1.5; margin-bottom: 12px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",