@primestyleai/tryon 5.8.58 → 5.9.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.
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { P as PrimeStyleError, L as LOCALE_LABELS, b as SUPPORTED_LOCALES, a as compressImage, c as createT, A as ApiClient, S as SseClient, i as isValidImageFile } from "../index-J6U0-q_3.js";
2
+ import { P as PrimeStyleError, L as LOCALE_LABELS, b as SUPPORTED_LOCALES, a as compressImage, c as createT, A as ApiClient, S as SseClient, i as isValidImageFile } from "../index-CuIieeOy.js";
3
3
  import { jsxs, jsx, Fragment } from "react/jsx-runtime";
4
4
  import { useState, useRef, useCallback, useEffect, useMemo } from "react";
5
5
  import { createPortal } from "react-dom";
@@ -1488,12 +1488,22 @@ const STYLES = `
1488
1488
  }
1489
1489
  .ps-tryon-v2-processing-label {
1490
1490
  position: absolute; bottom: 1vw; left: 50%; transform: translateX(-50%);
1491
- z-index: 5; font-size: 0.65vw; font-weight: 500;
1492
- color: var(--ps-accent); letter-spacing: 0.06em;
1493
- background: rgba(0,0,0,0.6); backdrop-filter: blur(8px);
1494
- padding: 0.25vw 0.8vw; border-radius: 2vw;
1491
+ z-index: 5; font-size: 0.7vw; font-weight: 600;
1492
+ color: #fff; letter-spacing: 0.05em;
1493
+ background: rgba(0,0,0,0.72); backdrop-filter: blur(10px);
1494
+ padding: 0.6vw 0.9vw; border-radius: 0.6vw;
1495
+ display: flex; flex-direction: column; align-items: center; gap: 0.5vw;
1496
+ min-width: 14vw;
1497
+ box-shadow: 0 0.4vw 1.5vw rgba(0,0,0,0.35);
1498
+ }
1499
+ .ps-tryon-v2-processing-label > span:first-child {
1495
1500
  animation: ps-loading-pulse 2s ease-in-out infinite;
1496
1501
  }
1502
+ .ps-tryon-v2-processing-label .ps-tryon-progress-ring-track { stroke: rgba(255,255,255,0.18); }
1503
+ .ps-tryon-v2-processing-label .ps-tryon-progress-ring-fill { stroke: var(--ps-accent-light); }
1504
+ .ps-tryon-v2-processing-label .ps-tryon-progress-eta { color: #fff; }
1505
+ .ps-tryon-v2-processing-label .ps-tryon-progress-bar-wrap { background: rgba(255,255,255,0.18); }
1506
+ .ps-tryon-v2-processing-label .ps-tryon-progress-pct { color: var(--ps-accent-light); }
1497
1507
 
1498
1508
  /* "I don't know" link */
1499
1509
  .ps-tryon-v2-dontknow {
@@ -2084,22 +2094,81 @@ const STYLES = `
2084
2094
  }
2085
2095
 
2086
2096
  .ps-tryon-progress-section {
2087
- display: flex; align-items: center; gap: 0.63vw; width: 100%; max-width: 15.6vw; margin-bottom: 0.83vw;
2097
+ display: flex; align-items: center; gap: 0.63vw; width: 100%; max-width: 18vw; margin-bottom: 0.83vw;
2098
+ }
2099
+ /* Shared progress layout used inside StageCycler (desktop) and
2100
+ MobileScanningView — row of ring + bar + percent, same tokens. */
2101
+ .ps-tryon-progress-wrap {
2102
+ display: flex; align-items: center; gap: 10px;
2103
+ width: 100%; max-width: 320px; margin-top: 16px;
2104
+ }
2105
+ .ps-tryon-progress-wrap .ps-tryon-progress-bar-wrap {
2106
+ flex: 1; height: 4px; border-radius: 3px; overflow: hidden;
2107
+ position: relative; background: var(--ps-border-color);
2108
+ }
2109
+ .ps-tryon-progress-wrap .ps-tryon-progress-bar-fill {
2110
+ height: 100%; width: 0%;
2111
+ background: linear-gradient(90deg, var(--ps-accent), var(--ps-accent-light));
2112
+ border-radius: 3px; transition: width 0.3s ease;
2113
+ }
2114
+ .ps-tryon-progress-wrap .ps-tryon-progress-pct {
2115
+ font-size: 11px; font-weight: 700; color: var(--ps-accent);
2116
+ min-width: 30px; text-align: right;
2117
+ font-variant-numeric: tabular-nums;
2088
2118
  }
2089
2119
  .ps-tryon-progress-bar-wrap {
2090
2120
  flex: 1; height: 0.31vw; background: var(--ps-border-color); border-radius: 3px; overflow: hidden;
2121
+ position: relative;
2122
+ }
2123
+ .ps-tryon-progress-bar-wrap::after {
2124
+ content: ""; position: absolute; inset: 0;
2125
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.45) 50%, transparent 100%);
2126
+ transform: translateX(-100%);
2127
+ animation: ps-progress-shimmer 2s linear infinite;
2128
+ pointer-events: none;
2129
+ }
2130
+ @keyframes ps-progress-shimmer {
2131
+ 0% { transform: translateX(-100%); }
2132
+ 100% { transform: translateX(100%); }
2091
2133
  }
2092
2134
  .ps-tryon-progress-bar-fill {
2093
2135
  height: 100%; background: linear-gradient(90deg, var(--ps-accent), var(--ps-accent-light));
2094
2136
  border-radius: 3px; transition: width 0.3s ease; width: 0%;
2137
+ position: relative; z-index: 1;
2095
2138
  }
2096
2139
  .ps-tryon-progress-pct {
2097
2140
  font-size: 0.68vw; font-weight: 700; color: var(--ps-accent); min-width: 1.88vw; text-align: right;
2098
2141
  font-variant-numeric: tabular-nums;
2099
2142
  }
2100
2143
 
2144
+ /* Circular ETA ring — 48×48 px SVG with a track + progress circle; ETA
2145
+ text centered. strokeDashoffset is driven by the ticker in
2146
+ PrimeStyleTryonInner, so CSS only styles the appearance. */
2147
+ .ps-tryon-progress-ring {
2148
+ position: relative; width: 48px; height: 48px; flex: 0 0 48px;
2149
+ display: flex; align-items: center; justify-content: center;
2150
+ }
2151
+ .ps-tryon-progress-ring svg { transform: rotate(-90deg); }
2152
+ .ps-tryon-progress-ring-track {
2153
+ fill: none; stroke: var(--ps-border-color); stroke-width: 3.5;
2154
+ }
2155
+ .ps-tryon-progress-ring-fill {
2156
+ fill: none; stroke: var(--ps-accent); stroke-width: 3.5;
2157
+ stroke-linecap: round;
2158
+ transition: stroke-dashoffset 0.3s ease;
2159
+ }
2160
+ .ps-tryon-progress-eta {
2161
+ position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
2162
+ font-size: 10px; font-weight: 700; color: var(--ps-accent);
2163
+ font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
2164
+ pointer-events: none;
2165
+ }
2166
+
2101
2167
  @keyframes ps-scale-in { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } }
2102
- .ps-tryon-processing-text { font-size: 0.73vw; color: var(--ps-text-primary); margin: 0 0 0.21vw; }
2168
+ .ps-tryon-processing-text {
2169
+ font-size: 0.73vw; color: var(--ps-text-primary); margin: 0 0 0.21vw;
2170
+ opacity: 1; transition: opacity 0.18s ease;
2171
+ }
2103
2172
  .ps-tryon-processing-sub { font-size: 0.63vw; color: var(--ps-text-secondary); margin: 0; }
2104
2173
 
2105
2174
  /* Result — split layout */
@@ -7412,17 +7481,71 @@ function MobileSkeleton({ landmarks, w, h }) {
7412
7481
  }
7413
7482
  );
7414
7483
  }
7484
+ const MSC_TRYON_TARGET_SECONDS = 22;
7485
+ const MSC_RING_RADIUS = 20;
7486
+ const MSC_RING_CIRC = 2 * Math.PI * MSC_RING_RADIUS;
7487
+ function MscTryOnProgress({ t }) {
7488
+ const startRef = useRef(Date.now());
7489
+ const ringRef = useRef(null);
7490
+ const barRef = useRef(null);
7491
+ const etaRef = useRef(null);
7492
+ const pctRef = useRef(null);
7493
+ useEffect(() => {
7494
+ startRef.current = Date.now();
7495
+ const id = setInterval(() => {
7496
+ const elapsed = (Date.now() - startRef.current) / 1e3;
7497
+ const pct = Math.min(95, elapsed / MSC_TRYON_TARGET_SECONDS * 100);
7498
+ const val = Math.round(pct);
7499
+ if (barRef.current) barRef.current.style.width = `${val}%`;
7500
+ if (pctRef.current) pctRef.current.textContent = `${val}%`;
7501
+ if (ringRef.current) ringRef.current.style.strokeDashoffset = String(MSC_RING_CIRC * (1 - pct / 100));
7502
+ if (etaRef.current) {
7503
+ const remaining = Math.max(0, MSC_TRYON_TARGET_SECONDS - Math.floor(elapsed));
7504
+ etaRef.current.textContent = elapsed >= MSC_TRYON_TARGET_SECONDS ? t("Finalizing...") : `~${remaining}s`;
7505
+ }
7506
+ }, 200);
7507
+ return () => clearInterval(id);
7508
+ }, [t]);
7509
+ return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-wrap", children: [
7510
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-ring", children: [
7511
+ /* @__PURE__ */ jsxs("svg", { width: "48", height: "48", viewBox: "0 0 48 48", "aria-hidden": "true", children: [
7512
+ /* @__PURE__ */ jsx("circle", { cx: "24", cy: "24", r: MSC_RING_RADIUS, className: "ps-tryon-progress-ring-track" }),
7513
+ /* @__PURE__ */ jsx(
7514
+ "circle",
7515
+ {
7516
+ ref: ringRef,
7517
+ cx: "24",
7518
+ cy: "24",
7519
+ r: MSC_RING_RADIUS,
7520
+ className: "ps-tryon-progress-ring-fill",
7521
+ strokeDasharray: MSC_RING_CIRC,
7522
+ strokeDashoffset: MSC_RING_CIRC
7523
+ }
7524
+ )
7525
+ ] }),
7526
+ /* @__PURE__ */ jsx("span", { ref: etaRef, className: "ps-tryon-progress-eta", children: `~${MSC_TRYON_TARGET_SECONDS}s` })
7527
+ ] }),
7528
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-progress-bar-wrap", children: /* @__PURE__ */ jsx("div", { ref: barRef, className: "ps-tryon-progress-bar-fill" }) }),
7529
+ /* @__PURE__ */ jsx("span", { ref: pctRef, className: "ps-tryon-progress-pct", children: "0%" })
7530
+ ] });
7531
+ }
7415
7532
  function MobileScanningView({
7416
7533
  previewUrl,
7417
7534
  productImage,
7418
7535
  bodyLandmarks,
7419
7536
  sizingDone,
7537
+ tryOnProcessing,
7420
7538
  onSwitchToManual,
7421
7539
  t
7422
7540
  }) {
7423
7541
  const displayImage = previewUrl || productImage || "";
7424
7542
  const isPhotoMode = !!previewUrl;
7425
- const stages = isPhotoMode ? [
7543
+ const stages = tryOnProcessing ? [
7544
+ { title: t("GENERATING TRY-ON"), desc: t("Rendering the garment on your photo."), viewfinderText: t("GENERATING") },
7545
+ { title: t("REFINING DETAILS"), desc: t("Fine-tuning fit, drape and shadows."), viewfinderText: t("REFINING") },
7546
+ { title: t("ALMOST THERE"), desc: t("Final compositing in progress."), viewfinderText: t("COMPOSITING") },
7547
+ { title: t("FINISHING TOUCHES"), desc: t("Polishing the result."), viewfinderText: t("FINISHING") }
7548
+ ] : isPhotoMode ? [
7426
7549
  { title: t("DETECTING POSE"), desc: t("Identifying body landmarks from your photo."), viewfinderText: t("DETECTING POSE") },
7427
7550
  { title: t("SCANNING FRAME"), desc: t("Our AI is mapping your proportions to calculate the perfect fit."), viewfinderText: t("SCANNING FRAME") },
7428
7551
  { title: t("ANALYZING BODY"), desc: t("Measuring shoulders, chest, waist and hips."), viewfinderText: t("ANALYZING") },
@@ -7465,10 +7588,13 @@ function MobileScanningView({
7465
7588
  ),
7466
7589
  isPhotoMode && bodyLandmarks && /* @__PURE__ */ jsx("div", { className: "ps-msc-pose-wrap", children: /* @__PURE__ */ jsx(MobileSkeleton, { landmarks: bodyLandmarks, w: dims.w, h: dims.h }) })
7467
7590
  ] }),
7468
- /* @__PURE__ */ jsx("div", { className: "ps-msc-stage", children: /* @__PURE__ */ jsxs("div", { className: "ps-msc-stage-slot", children: [
7469
- /* @__PURE__ */ jsx("div", { className: "ps-msc-stage-title", children: current.title }),
7470
- /* @__PURE__ */ jsx("div", { className: "ps-msc-stage-desc", children: current.desc })
7471
- ] }, stageIdx) }),
7591
+ /* @__PURE__ */ jsxs("div", { className: "ps-msc-stage", children: [
7592
+ /* @__PURE__ */ jsxs("div", { className: "ps-msc-stage-slot", children: [
7593
+ /* @__PURE__ */ jsx("div", { className: "ps-msc-stage-title", children: current.title }),
7594
+ /* @__PURE__ */ jsx("div", { className: "ps-msc-stage-desc", children: current.desc })
7595
+ ] }, stageIdx),
7596
+ tryOnProcessing && /* @__PURE__ */ jsx(MscTryOnProgress, { t })
7597
+ ] }),
7472
7598
  /* @__PURE__ */ jsx("div", { className: "ps-bpm-spacer" }),
7473
7599
  /* @__PURE__ */ jsx("div", { className: "ps-bpm-bottom", children: /* @__PURE__ */ jsx(
7474
7600
  MobileBottomTabs,
@@ -7666,6 +7792,62 @@ const SKELETON_CONNECTIONS = [
7666
7792
  ["rightHip", "rightKnee"],
7667
7793
  ["rightKnee", "rightAnkle"]
7668
7794
  ];
7795
+ const TRYON_TARGET_SECONDS = 22;
7796
+ const TRYON_RING_RADIUS = 20;
7797
+ const TRYON_RING_CIRC = 2 * Math.PI * TRYON_RING_RADIUS;
7798
+ function TryOnProgress({ t, isActive }) {
7799
+ const startRef = useRef(null);
7800
+ const ringRef = useRef(null);
7801
+ const barRef = useRef(null);
7802
+ const etaRef = useRef(null);
7803
+ const pctRef = useRef(null);
7804
+ useEffect(() => {
7805
+ if (!isActive) {
7806
+ startRef.current = null;
7807
+ return;
7808
+ }
7809
+ startRef.current = Date.now();
7810
+ const id = setInterval(() => {
7811
+ const start = startRef.current || Date.now();
7812
+ const elapsed = (Date.now() - start) / 1e3;
7813
+ const pct = Math.min(95, elapsed / TRYON_TARGET_SECONDS * 100);
7814
+ const val = Math.round(pct);
7815
+ if (barRef.current) barRef.current.style.width = `${val}%`;
7816
+ if (pctRef.current) pctRef.current.textContent = `${val}%`;
7817
+ if (ringRef.current) {
7818
+ ringRef.current.style.strokeDashoffset = String(TRYON_RING_CIRC * (1 - pct / 100));
7819
+ }
7820
+ if (etaRef.current) {
7821
+ const remaining = Math.max(0, TRYON_TARGET_SECONDS - Math.floor(elapsed));
7822
+ etaRef.current.textContent = elapsed >= TRYON_TARGET_SECONDS ? t("Finalizing...") : `~${remaining}s`;
7823
+ }
7824
+ }, 200);
7825
+ return () => clearInterval(id);
7826
+ }, [isActive, t]);
7827
+ if (!isActive) return null;
7828
+ return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-wrap", children: [
7829
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-ring", children: [
7830
+ /* @__PURE__ */ jsxs("svg", { width: "48", height: "48", viewBox: "0 0 48 48", "aria-hidden": "true", children: [
7831
+ /* @__PURE__ */ jsx("circle", { cx: "24", cy: "24", r: TRYON_RING_RADIUS, className: "ps-tryon-progress-ring-track" }),
7832
+ /* @__PURE__ */ jsx(
7833
+ "circle",
7834
+ {
7835
+ ref: ringRef,
7836
+ cx: "24",
7837
+ cy: "24",
7838
+ r: TRYON_RING_RADIUS,
7839
+ className: "ps-tryon-progress-ring-fill",
7840
+ strokeDasharray: TRYON_RING_CIRC,
7841
+ strokeDashoffset: TRYON_RING_CIRC
7842
+ }
7843
+ )
7844
+ ] }),
7845
+ /* @__PURE__ */ jsx("span", { ref: etaRef, className: "ps-tryon-progress-eta", children: `~${TRYON_TARGET_SECONDS}s` })
7846
+ ] }),
7847
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-progress-bar-wrap", children: /* @__PURE__ */ jsx("div", { ref: barRef, className: "ps-tryon-progress-bar-fill" }) }),
7848
+ /* @__PURE__ */ jsx("span", { ref: pctRef, className: "ps-tryon-progress-pct", children: "0%" })
7849
+ ] });
7850
+ }
7669
7851
  function FaceOverlay({
7670
7852
  landmarks,
7671
7853
  imgWidth,
@@ -7809,10 +7991,13 @@ function StageCycler({
7809
7991
  return () => clearInterval(id);
7810
7992
  }, [isDone, active.length]);
7811
7993
  const current = active[idx] ?? active[0];
7812
- return /* @__PURE__ */ jsx("div", { className: "ps-msc-stage", style: { alignSelf: "center", marginTop: "auto", marginBottom: "auto" }, children: /* @__PURE__ */ jsxs("div", { className: "ps-msc-stage-slot", children: [
7813
- /* @__PURE__ */ jsx("div", { className: "ps-msc-stage-title", children: current.title }),
7814
- /* @__PURE__ */ jsx("div", { className: "ps-msc-stage-desc", children: current.desc })
7815
- ] }, `${tryOnProcessing ? "t" : "s"}-${idx}`) });
7994
+ return /* @__PURE__ */ jsxs("div", { className: "ps-msc-stage", style: { alignSelf: "center", marginTop: "auto", marginBottom: "auto" }, children: [
7995
+ /* @__PURE__ */ jsxs("div", { className: "ps-msc-stage-slot", children: [
7996
+ /* @__PURE__ */ jsx("div", { className: "ps-msc-stage-title", children: current.title }),
7997
+ /* @__PURE__ */ jsx("div", { className: "ps-msc-stage-desc", children: current.desc })
7998
+ ] }, `${tryOnProcessing ? "t" : "s"}-${idx}`),
7999
+ tryOnProcessing && /* @__PURE__ */ jsx(TryOnProgress, { t, isActive: !!tryOnProcessing })
8000
+ ] });
7816
8001
  }
7817
8002
  function SkeletonOverlay({ landmarks, imgWidth, imgHeight }) {
7818
8003
  const W = imgWidth;
@@ -8950,6 +9135,7 @@ function SizeResultView({
8950
9135
  const allDone = hasPhoto ? sizingDone && tryOnDone : sizingDone;
8951
9136
  const isMobile = useIsMobile();
8952
9137
  const isAccessory = measurementType === "face" || measurementType === "head";
9138
+ const vtoExcluded = measurementType === "foot";
8953
9139
  console.log("[PS-SDK] SizeResultView render:", {
8954
9140
  hasPhoto,
8955
9141
  isSnapProcessing,
@@ -8987,6 +9173,7 @@ function SizeResultView({
8987
9173
  previewUrl,
8988
9174
  bodyLandmarks: bodyLandmarks ?? null,
8989
9175
  sizingDone,
9176
+ tryOnProcessing,
8990
9177
  onSwitchToManual: () => setView("body-profile"),
8991
9178
  t
8992
9179
  }
@@ -9043,7 +9230,7 @@ function SizeResultView({
9043
9230
  isMobile: true,
9044
9231
  isTryOnImage: !!resultImageUrl,
9045
9232
  showLines,
9046
- onToggleLines: () => setShowLines(!showLines),
9233
+ onToggleLines: isAccessory ? void 0 : () => setShowLines(!showLines),
9047
9234
  onImageLoad: handleImgLoad,
9048
9235
  overlayNode: resultImageUrl && poseReady && poseLines ? /* @__PURE__ */ jsx(
9049
9236
  MeasurementOverlay,
@@ -9117,7 +9304,7 @@ function SizeResultView({
9117
9304
  },
9118
9305
  onClose,
9119
9306
  showLines,
9120
- onToggleLines: () => setShowLines(!showLines),
9307
+ onToggleLines: isAccessory ? void 0 : () => setShowLines(!showLines),
9121
9308
  onImageLoad: handleImgLoad,
9122
9309
  overlayNode: resultImageUrl && poseReady && poseLines ? /* @__PURE__ */ jsx(
9123
9310
  MeasurementOverlay,
@@ -9152,7 +9339,10 @@ function SizeResultView({
9152
9339
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-v2-bg", style: { position: "relative" }, children: [
9153
9340
  /* @__PURE__ */ jsx("img", { src: tryOnProcessing && previewUrl ? previewUrl : resultImageUrl || productImage, alt: productTitle, className: "ps-tryon-v2-bg-img", onLoad: handleImgLoad }),
9154
9341
  tryOnProcessing && bodyLandmarks && /* @__PURE__ */ jsx(SkeletonOverlay, { landmarks: bodyLandmarks, imgWidth: imgDims.w, imgHeight: imgDims.h }),
9155
- tryOnProcessing && /* @__PURE__ */ jsx("div", { className: "ps-tryon-v2-processing-label", children: t("Generating try-on...") }),
9342
+ tryOnProcessing && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-v2-processing-label", children: [
9343
+ /* @__PURE__ */ jsx("span", { children: t("Generating try-on...") }),
9344
+ /* @__PURE__ */ jsx(TryOnProgress, { t, isActive: true })
9345
+ ] }),
9156
9346
  resultImageUrl && !tryOnProcessing && poseReady && poseLines && /* @__PURE__ */ jsx(MeasurementOverlay, { lines: poseLines, fitRows: (() => {
9157
9347
  const all = [...sizingResult?.matchDetails || []];
9158
9348
  if (sizingResult?.sections) {
@@ -9169,7 +9359,7 @@ function SizeResultView({
9169
9359
  }).map((m) => ({ area: m.measurement, userNum: parseFloat(m.userValue) || 0, chartLabel: m.chartRange || "", fit: m.fit }));
9170
9360
  })(), show: showLines, imgWidth: imgDims.w, imgHeight: imgDims.h }),
9171
9361
  resultImageUrl && !tryOnProcessing && /* @__PURE__ */ jsxs("div", { style: { position: "absolute", bottom: "0.5vw", left: "0.5vw", zIndex: 3, display: "flex", flexDirection: "column", gap: "0.3vw" }, children: [
9172
- /* @__PURE__ */ jsxs("button", { className: "ps-tryon-sr-glass-btn", onClick: () => setShowLines(!showLines), children: [
9362
+ !isAccessory && /* @__PURE__ */ jsxs("button", { className: "ps-tryon-sr-glass-btn", onClick: () => setShowLines(!showLines), children: [
9173
9363
  /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", style: { marginRight: "0.3vw" }, children: [
9174
9364
  /* @__PURE__ */ jsx("line", { x1: "4", y1: "9", x2: "20", y2: "9" }),
9175
9365
  /* @__PURE__ */ jsx("line", { x1: "4", y1: "15", x2: "20", y2: "15" }),
@@ -9223,7 +9413,7 @@ function SizeResultView({
9223
9413
  " →"
9224
9414
  ]
9225
9415
  }
9226
- ) : isAccessory ? /* @__PURE__ */ jsxs(
9416
+ ) : vtoExcluded ? /* @__PURE__ */ jsxs(
9227
9417
  "button",
9228
9418
  {
9229
9419
  className: "ps-tryon-v2-cta",
@@ -9283,7 +9473,7 @@ function SizeResultView({
9283
9473
  onBack: resultImageUrl ? onClose || (() => setView("body-profile")) : () => setView("body-profile"),
9284
9474
  backLabel: t("Back"),
9285
9475
  internationalSizes: sizingResult?.internationalSizes,
9286
- onTryOn: resultImageUrl || isAccessory ? void 0 : handleSingleTryOn,
9476
+ onTryOn: resultImageUrl || vtoExcluded ? void 0 : handleSingleTryOn,
9287
9477
  continueLabel: resultImageUrl ? t("Continue Shopping") : void 0,
9288
9478
  tryOnProcessing,
9289
9479
  productImage: resultImageUrl || productImage,
@@ -9292,7 +9482,7 @@ function SizeResultView({
9292
9482
  renderRaw: isAccessory,
9293
9483
  isTryOnImage: !!resultImageUrl,
9294
9484
  showLines,
9295
- onToggleLines: () => setShowLines(!showLines),
9485
+ onToggleLines: isAccessory ? void 0 : () => setShowLines(!showLines),
9296
9486
  onImageLoad: handleImgLoad,
9297
9487
  overlayNode: resultImageUrl && poseReady && poseLines ? /* @__PURE__ */ jsx(
9298
9488
  MeasurementOverlay,
@@ -9313,7 +9503,7 @@ function SizeResultView({
9313
9503
  /* @__PURE__ */ jsx("img", { src: resultImageUrl || productImage, alt: productTitle, className: "ps-tryon-v2-bg-img", onLoad: handleImgLoad }),
9314
9504
  resultImageUrl && poseReady && poseLines && /* @__PURE__ */ jsx(MeasurementOverlay, { lines: poseLines, fitRows: (sizingResult?.matchDetails || []).map((m) => ({ area: m.measurement, userNum: parseFloat(m.userValue) || 0, chartLabel: m.chartRange || "", fit: m.fit })), show: showLines, imgWidth: imgDims.w, imgHeight: imgDims.h }),
9315
9505
  resultImageUrl && !tryOnProcessing && /* @__PURE__ */ jsxs("div", { style: { position: "absolute", bottom: "0.5vw", left: "0.5vw", zIndex: 3, display: "flex", flexDirection: "column", gap: "0.3vw" }, children: [
9316
- /* @__PURE__ */ jsx("button", { className: "ps-tryon-sr-glass-btn", onClick: () => setShowLines(!showLines), children: showLines ? t("Hide Fit") : t("Show Fit") }),
9506
+ !isAccessory && /* @__PURE__ */ jsx("button", { className: "ps-tryon-sr-glass-btn", onClick: () => setShowLines(!showLines), children: showLines ? t("Hide Fit") : t("Show Fit") }),
9317
9507
  /* @__PURE__ */ jsx("button", { className: "ps-tryon-sr-glass-btn", onClick: handleDownload, children: t("Download") })
9318
9508
  ] })
9319
9509
  ] }),
@@ -9330,7 +9520,7 @@ function SizeResultView({
9330
9520
  onBack: resultImageUrl ? onClose || (() => setView("body-profile")) : () => setView("body-profile"),
9331
9521
  backLabel: t("Back"),
9332
9522
  internationalSizes: sizingResult?.internationalSizes,
9333
- onTryOn: resultImageUrl || isAccessory ? void 0 : handleSingleTryOn,
9523
+ onTryOn: resultImageUrl || vtoExcluded ? void 0 : handleSingleTryOn,
9334
9524
  continueLabel: resultImageUrl ? t("Continue Shopping") : void 0,
9335
9525
  tryOnProcessing,
9336
9526
  t,
@@ -9340,14 +9530,14 @@ function SizeResultView({
9340
9530
  ] });
9341
9531
  })()
9342
9532
  ),
9343
- showPhotoGuide && !isAccessory && /* @__PURE__ */ jsx("div", { className: "ps-tryon-sr-chart-overlay", children: isMobile ? (
9533
+ showPhotoGuide && !vtoExcluded && /* @__PURE__ */ jsx("div", { className: "ps-tryon-sr-chart-overlay", children: isMobile ? (
9344
9534
  /* ── Mobile: same layout as the AI-sizing photo step
9345
9535
  (PhotoStepMobile) — title + subtitle, large preview,
9346
9536
  checklist card, primary CTA + RETAKE secondary. ── */
9347
9537
  /* @__PURE__ */ jsx("div", { className: "ps-bp-wrapper", style: { padding: "16px 16px 0", background: "var(--ps-bg-primary)" }, children: /* @__PURE__ */ jsxs("div", { className: "ps-pm-root", children: [
9348
9538
  /* @__PURE__ */ jsxs("div", { className: "ps-pm-header", children: [
9349
9539
  /* @__PURE__ */ jsx("h2", { className: "ps-pm-title", children: t("Review your photo") }),
9350
- /* @__PURE__ */ jsx("p", { className: "ps-pm-subtitle", children: t("Ensure your full body is visible for the most accurate virtual try-on.") })
9540
+ /* @__PURE__ */ jsx("p", { className: "ps-pm-subtitle", children: measurementType === "face" ? t("A clear, front-facing face photo — no glasses on — gives us the most accurate try-on.") : measurementType === "head" ? t("Face the camera with your head and shoulders in frame, leaving space above your head.") : t("Ensure your full body is visible for the most accurate virtual try-on.") })
9351
9541
  ] }),
9352
9542
  /* @__PURE__ */ jsx(
9353
9543
  "input",
@@ -9398,11 +9588,19 @@ function SizeResultView({
9398
9588
  /* @__PURE__ */ jsx("div", { className: "ps-pm-checklist-icon", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", width: "14", height: "14", children: /* @__PURE__ */ jsx("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" }) }) }),
9399
9589
  /* @__PURE__ */ jsxs("div", { className: "ps-pm-checklist-body", children: [
9400
9590
  /* @__PURE__ */ jsx("div", { className: "ps-pm-checklist-title", children: t("Checklist for accuracy") }),
9401
- /* @__PURE__ */ jsxs("ul", { className: "ps-pm-checklist-items", children: [
9591
+ /* @__PURE__ */ jsx("ul", { className: "ps-pm-checklist-items", children: measurementType === "face" ? /* @__PURE__ */ jsxs(Fragment, { children: [
9592
+ /* @__PURE__ */ jsx("li", { children: t("Face the camera directly at eye level") }),
9593
+ /* @__PURE__ */ jsx("li", { children: t("Remove any glasses you're wearing") }),
9594
+ /* @__PURE__ */ jsx("li", { children: t("Good lighting, plain background") })
9595
+ ] }) : measurementType === "head" ? /* @__PURE__ */ jsxs(Fragment, { children: [
9596
+ /* @__PURE__ */ jsx("li", { children: t("Head and shoulders in frame") }),
9597
+ /* @__PURE__ */ jsx("li", { children: t("Leave space above your head") }),
9598
+ /* @__PURE__ */ jsx("li", { children: t("Good lighting, plain background") })
9599
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
9402
9600
  /* @__PURE__ */ jsx("li", { children: t("Form-fitting clothing is recommended") }),
9403
9601
  /* @__PURE__ */ jsx("li", { children: t("Standing 2-3 meters from camera") }),
9404
9602
  /* @__PURE__ */ jsx("li", { children: t("Neutral background with good lighting") })
9405
- ] })
9603
+ ] }) })
9406
9604
  ] })
9407
9605
  ] }),
9408
9606
  /* @__PURE__ */ jsx("div", { className: "ps-bpm-spacer" }),
@@ -9482,7 +9680,7 @@ function SizeResultView({
9482
9680
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
9483
9681
  /* @__PURE__ */ jsx(UploadIcon, { size: 32 }),
9484
9682
  /* @__PURE__ */ jsx("span", { style: { fontSize: "0.85vw", fontWeight: 600, color: "var(--ps-text-primary)", marginTop: "0.5vw" }, children: t("Upload your photo") }),
9485
- /* @__PURE__ */ jsx("span", { style: { fontSize: "0.6vw", color: "var(--ps-text-muted)", marginTop: "0.2vw" }, children: t("Click or drag a full-body photo") })
9683
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "0.6vw", color: "var(--ps-text-muted)", marginTop: "0.2vw" }, children: measurementType === "face" ? t("Click or drag a close-up face photo") : measurementType === "head" ? t("Click or drag a head-and-shoulders photo") : t("Click or drag a full-body photo") })
9486
9684
  ] }),
9487
9685
  /* @__PURE__ */ jsx(
9488
9686
  "input",
@@ -9507,7 +9705,23 @@ function SizeResultView({
9507
9705
  /* @__PURE__ */ jsx("span", { style: { color: "#1c9d4c", fontSize: "0.75vw", fontWeight: 700 }, children: "✓" }),
9508
9706
  /* @__PURE__ */ jsx("span", { style: { color: "#1c9d4c", fontSize: "0.65vw", fontWeight: 600 }, children: t("Do") })
9509
9707
  ] }),
9510
- /* @__PURE__ */ jsxs("div", { style: { fontSize: "0.58vw", color: "var(--ps-text-primary)", lineHeight: 1.8 }, children: [
9708
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "0.58vw", color: "var(--ps-text-primary)", lineHeight: 1.8 }, children: measurementType === "face" ? /* @__PURE__ */ jsxs(Fragment, { children: [
9709
+ t("Face the camera directly, centered in frame"),
9710
+ /* @__PURE__ */ jsx("br", {}),
9711
+ t("Use natural, even lighting (e.g. near a window)"),
9712
+ /* @__PURE__ */ jsx("br", {}),
9713
+ t("Keep hair away from your face and ears"),
9714
+ /* @__PURE__ */ jsx("br", {}),
9715
+ t("Choose a plain, light background")
9716
+ ] }) : measurementType === "head" ? /* @__PURE__ */ jsxs(Fragment, { children: [
9717
+ t("Face the camera with head and shoulders in frame"),
9718
+ /* @__PURE__ */ jsx("br", {}),
9719
+ t("Leave some space above your head"),
9720
+ /* @__PURE__ */ jsx("br", {}),
9721
+ t("Use natural, even lighting"),
9722
+ /* @__PURE__ */ jsx("br", {}),
9723
+ t("Choose a plain, light background")
9724
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
9511
9725
  t("Stand facing the camera with your full body in frame"),
9512
9726
  /* @__PURE__ */ jsx("br", {}),
9513
9727
  t("Use natural or even lighting"),
@@ -9515,14 +9729,30 @@ function SizeResultView({
9515
9729
  t("Wear fitted, simple clothing"),
9516
9730
  /* @__PURE__ */ jsx("br", {}),
9517
9731
  t("Stand straight and still, arms relaxed")
9518
- ] })
9732
+ ] }) })
9519
9733
  ] }),
9520
9734
  /* @__PURE__ */ jsxs("div", { style: { background: "#ffe2e2", borderRadius: "0.5vw", padding: "0.6vw 0.8vw" }, children: [
9521
9735
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.3vw", marginBottom: "0.3vw" }, children: [
9522
9736
  /* @__PURE__ */ jsx("span", { style: { color: "#e7000b", fontSize: "0.75vw", fontWeight: 700 }, children: "✗" }),
9523
9737
  /* @__PURE__ */ jsx("span", { style: { color: "#e7000b", fontSize: "0.65vw", fontWeight: 600 }, children: t("Don't") })
9524
9738
  ] }),
9525
- /* @__PURE__ */ jsxs("div", { style: { fontSize: "0.58vw", color: "var(--ps-text-primary)", lineHeight: 1.8 }, children: [
9739
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "0.58vw", color: "var(--ps-text-primary)", lineHeight: 1.8 }, children: measurementType === "face" ? /* @__PURE__ */ jsxs(Fragment, { children: [
9740
+ t("Don't wear sunglasses or a hat in the photo"),
9741
+ /* @__PURE__ */ jsx("br", {}),
9742
+ t("Don't tilt or turn your head"),
9743
+ /* @__PURE__ */ jsx("br", {}),
9744
+ t("Don't use strong backlighting or flash"),
9745
+ /* @__PURE__ */ jsx("br", {}),
9746
+ t("Don't apply filters or edits")
9747
+ ] }) : measurementType === "head" ? /* @__PURE__ */ jsxs(Fragment, { children: [
9748
+ t("Don't wear a hat in the photo"),
9749
+ /* @__PURE__ */ jsx("br", {}),
9750
+ t("Don't crop out the top of your head"),
9751
+ /* @__PURE__ */ jsx("br", {}),
9752
+ t("Don't use strong backlighting or flash"),
9753
+ /* @__PURE__ */ jsx("br", {}),
9754
+ t("Don't apply filters or edits")
9755
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
9526
9756
  t("Don't wear loose or baggy clothing"),
9527
9757
  /* @__PURE__ */ jsx("br", {}),
9528
9758
  t("Don't sit, pose, or bend"),
@@ -9530,14 +9760,14 @@ function SizeResultView({
9530
9760
  t("Don't take mirror photos or selfies"),
9531
9761
  /* @__PURE__ */ jsx("br", {}),
9532
9762
  t("Don't apply filters or edits")
9533
- ] })
9763
+ ] }) })
9534
9764
  ] }),
9535
9765
  /* @__PURE__ */ jsxs("div", { style: { background: "rgba(59,130,246,0.08)", border: "1px solid rgba(59,130,246,0.2)", borderRadius: "0.5vw", padding: "0.5vw 0.8vw" }, children: [
9536
9766
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.3vw", marginBottom: "0.2vw" }, children: [
9537
9767
  /* @__PURE__ */ jsx(SparkleIcon, { size: 12 }),
9538
9768
  /* @__PURE__ */ jsx("span", { style: { color: "var(--ps-accent)", fontSize: "0.65vw", fontWeight: 700 }, children: t("Pro Tip") })
9539
9769
  ] }),
9540
- /* @__PURE__ */ jsx("div", { style: { fontSize: "0.55vw", color: "var(--ps-text-secondary)", lineHeight: 1.7 }, children: t("Our AI works best with front-facing, full-body photos in fitted clothing. Better photos = more accurate virtual try-on!") })
9770
+ /* @__PURE__ */ jsx("div", { style: { fontSize: "0.55vw", color: "var(--ps-text-secondary)", lineHeight: 1.7 }, children: measurementType === "face" ? t("A clear, well-lit face photo gives the most accurate eyewear try-on.") : measurementType === "head" ? t("A clear head-and-shoulders photo with space above your head gives the most accurate headwear try-on.") : t("Our AI works best with front-facing, full-body photos in fitted clothing. Better photos = more accurate virtual try-on!") })
9541
9771
  ] })
9542
9772
  ] })
9543
9773
  ] }),
@@ -9666,12 +9896,16 @@ function UploadView({
9666
9896
  }
9667
9897
  ) });
9668
9898
  }
9899
+ const RING_RADIUS = 20;
9900
+ const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
9669
9901
  function ProcessingView({
9670
9902
  previewUrl,
9671
9903
  progressRef,
9672
9904
  progressBarRef,
9673
9905
  progressTextRef,
9674
9906
  progressStatusRef,
9907
+ progressEtaRef,
9908
+ progressRingRef,
9675
9909
  cn,
9676
9910
  t
9677
9911
  }) {
@@ -9686,6 +9920,16 @@ function ProcessingView({
9686
9920
  const statusCb = useCallback((el) => {
9687
9921
  progressStatusRef.current = el;
9688
9922
  }, []);
9923
+ const etaCb = useCallback((el) => {
9924
+ progressEtaRef.current = el;
9925
+ }, []);
9926
+ const ringCb = useCallback((el) => {
9927
+ progressRingRef.current = el;
9928
+ if (el) {
9929
+ const offset = RING_CIRCUMFERENCE * (1 - Math.round(progressRef.current) / 100);
9930
+ el.style.strokeDashoffset = String(offset);
9931
+ }
9932
+ }, []);
9689
9933
  return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
9690
9934
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing-image-wrap", children: [
9691
9935
  previewUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -9696,6 +9940,32 @@ function ProcessingView({
9696
9940
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-overlay" })
9697
9941
  ] }),
9698
9942
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-section", children: [
9943
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-ring", children: [
9944
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 48 48", width: "48", height: "48", "aria-hidden": "true", children: [
9945
+ /* @__PURE__ */ jsx(
9946
+ "circle",
9947
+ {
9948
+ cx: "24",
9949
+ cy: "24",
9950
+ r: RING_RADIUS,
9951
+ className: "ps-tryon-progress-ring-track"
9952
+ }
9953
+ ),
9954
+ /* @__PURE__ */ jsx(
9955
+ "circle",
9956
+ {
9957
+ ref: ringCb,
9958
+ cx: "24",
9959
+ cy: "24",
9960
+ r: RING_RADIUS,
9961
+ className: "ps-tryon-progress-ring-fill",
9962
+ strokeDasharray: RING_CIRCUMFERENCE,
9963
+ strokeDashoffset: RING_CIRCUMFERENCE
9964
+ }
9965
+ )
9966
+ ] }),
9967
+ /* @__PURE__ */ jsx("span", { ref: etaCb, className: "ps-tryon-progress-eta", children: `~22s` })
9968
+ ] }),
9699
9969
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-progress-bar-wrap", children: /* @__PURE__ */ jsx("div", { ref: barCb, className: "ps-tryon-progress-bar-fill" }) }),
9700
9970
  /* @__PURE__ */ jsx("span", { ref: pctCb, className: "ps-tryon-progress-pct", children: "0%" })
9701
9971
  ] }),
@@ -11361,12 +11631,13 @@ function PhotoStepMobile({
11361
11631
  photoVariant = "full-body",
11362
11632
  photoStepHeight,
11363
11633
  onPhotoStepHeightChange,
11634
+ ageConfirmed,
11635
+ onAgeConfirmedChange,
11364
11636
  t
11365
11637
  }) {
11366
11638
  const isCloseUp = photoVariant === "close-up";
11367
11639
  const fileRef = useRef(null);
11368
11640
  const hasPhoto = !!photoPreview;
11369
- const [ageConfirmed, setAgeConfirmed] = useState(null);
11370
11641
  const gated = !hasPhoto && ageConfirmed !== true;
11371
11642
  return /* @__PURE__ */ jsxs("div", { className: "ps-pm-root", children: [
11372
11643
  /* @__PURE__ */ jsxs("div", { className: "ps-pm-header", children: [
@@ -11418,14 +11689,14 @@ function PhotoStepMobile({
11418
11689
  /* @__PURE__ */ jsx("div", { className: "ps-pm-age-gate-eyebrow", children: t("AGE VERIFICATION") }),
11419
11690
  /* @__PURE__ */ jsx("div", { className: "ps-pm-age-gate-question", children: t("Is the person in this photo 18 years or older?") }),
11420
11691
  /* @__PURE__ */ jsxs("div", { className: "ps-pm-age-gate-actions", children: [
11421
- /* @__PURE__ */ jsx("button", { type: "button", className: "ps-pm-age-gate-btn ps-pm-age-gate-btn-primary", onClick: () => setAgeConfirmed(true), children: t("Yes") }),
11422
- /* @__PURE__ */ jsx("button", { type: "button", className: "ps-pm-age-gate-btn ps-pm-age-gate-btn-secondary", onClick: () => setAgeConfirmed(false), children: t("No") })
11692
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ps-pm-age-gate-btn ps-pm-age-gate-btn-primary", onClick: () => onAgeConfirmedChange(true), children: t("Yes") }),
11693
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ps-pm-age-gate-btn ps-pm-age-gate-btn-secondary", onClick: () => onAgeConfirmedChange(false), children: t("No") })
11423
11694
  ] })
11424
11695
  ] }) }),
11425
11696
  ageConfirmed === false && /* @__PURE__ */ jsx("div", { className: "ps-pm-age-gate", role: "alert", children: /* @__PURE__ */ jsxs("div", { className: "ps-pm-age-gate-card", children: [
11426
11697
  /* @__PURE__ */ jsx("div", { className: "ps-pm-age-gate-eyebrow ps-pm-age-gate-eyebrow-blocked", children: t("UPLOAD NOT ALLOWED") }),
11427
11698
  /* @__PURE__ */ jsx("div", { className: "ps-pm-age-gate-question", children: t("For your safety, we cannot process photos of people under 18.") }),
11428
- /* @__PURE__ */ jsx("button", { type: "button", className: "ps-pm-age-gate-btn ps-pm-age-gate-btn-secondary", onClick: () => setAgeConfirmed(null), children: t("Go back") })
11699
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ps-pm-age-gate-btn ps-pm-age-gate-btn-secondary", onClick: () => onAgeConfirmedChange(null), children: t("Go back") })
11429
11700
  ] }) })
11430
11701
  ] }) }),
11431
11702
  /* @__PURE__ */ jsxs("div", { className: "ps-pm-legal-notice", children: [
@@ -11893,6 +12164,11 @@ function BodyProfileView({
11893
12164
  error,
11894
12165
  photoStepHeight,
11895
12166
  onPhotoStepHeightChange: setPhotoStepHeight,
12167
+ ageConfirmed,
12168
+ onAgeConfirmedChange: (v) => {
12169
+ setAgeConfirmed(v);
12170
+ if (v === true) setError("");
12171
+ },
11896
12172
  t
11897
12173
  }
11898
12174
  ) });
@@ -12639,6 +12915,11 @@ function AccessorySizeView({
12639
12915
  onSwitchToManual: () => setStep("manual"),
12640
12916
  error,
12641
12917
  photoVariant,
12918
+ ageConfirmed,
12919
+ onAgeConfirmedChange: (v) => {
12920
+ setAgeConfirmed(v);
12921
+ if (v === true) setError("");
12922
+ },
12642
12923
  t
12643
12924
  }
12644
12925
  ) });
@@ -13196,6 +13477,12 @@ function detectMeasurementType(title) {
13196
13477
  if (/\b(sunglass|sunglasses|eyewear|eyeglasses|glasses|spectacles|optical|goggles|frames|aviator|wayfarer|lens)\b/.test(t)) return "face";
13197
13478
  return "body";
13198
13479
  }
13480
+ function measurementTypeToVtoCategory(type) {
13481
+ if (type === "face") return "sunglasses";
13482
+ if (type === "head") return "hat";
13483
+ if (type === "body") return "apparel";
13484
+ return null;
13485
+ }
13199
13486
  function PrimeStyleTryonInner({
13200
13487
  productImage,
13201
13488
  productTitle = "Product",
@@ -13301,15 +13588,22 @@ function PrimeStyleTryonInner({
13301
13588
  if (pollingRef.current) clearInterval(pollingRef.current);
13302
13589
  };
13303
13590
  }, [apiUrl]);
13591
+ const TARGET_SECONDS = 22;
13304
13592
  const progressRef = useRef(0);
13305
13593
  const progressBarRef = useRef(null);
13306
13594
  const progressTextRef = useRef(null);
13307
13595
  const progressStatusRef = useRef(null);
13596
+ const progressEtaRef = useRef(null);
13597
+ const progressRingRef = useRef(null);
13598
+ const progressStartTsRef = useRef(null);
13599
+ const progressLastStageRef = useRef("");
13308
13600
  const progressIntervalRef = useRef(null);
13309
13601
  useEffect(() => {
13310
13602
  if (view === "processing") {
13311
13603
  if (progressIntervalRef.current) return;
13312
13604
  progressRef.current = 0;
13605
+ progressStartTsRef.current = Date.now();
13606
+ progressLastStageRef.current = "";
13313
13607
  const statuses = [
13314
13608
  { at: 0, text: t("Preparing your image...") },
13315
13609
  { at: 15, text: t("Analyzing body proportions...") },
@@ -13318,17 +13612,35 @@ function PrimeStyleTryonInner({
13318
13612
  { at: 75, text: t("Refining details...") },
13319
13613
  { at: 90, text: t("Almost there...") }
13320
13614
  ];
13615
+ const RING_CIRCUMFERENCE2 = 2 * Math.PI * 20;
13321
13616
  progressIntervalRef.current = setInterval(() => {
13322
- const p = progressRef.current;
13323
- if (p >= 100) return;
13324
- const increment = p < 30 ? 1.2 : p < 60 ? 0.8 : p < 80 ? 0.4 : p < 95 ? 0.15 : 0;
13325
- progressRef.current = Math.min(p + increment, 95);
13326
- const val = Math.round(progressRef.current);
13617
+ if (completedRef.current) return;
13618
+ const startTs = progressStartTsRef.current || Date.now();
13619
+ const elapsed = (Date.now() - startTs) / 1e3;
13620
+ const target = Math.min(95, elapsed / TARGET_SECONDS * 100);
13621
+ progressRef.current = target;
13622
+ const val = Math.round(target);
13327
13623
  if (progressBarRef.current) progressBarRef.current.style.width = `${val}%`;
13328
13624
  if (progressTextRef.current) progressTextRef.current.textContent = `${val}%`;
13625
+ if (progressRingRef.current) {
13626
+ const offset = RING_CIRCUMFERENCE2 * (1 - target / 100);
13627
+ progressRingRef.current.style.strokeDashoffset = String(offset);
13628
+ }
13629
+ if (progressEtaRef.current) {
13630
+ const remaining = Math.max(0, TARGET_SECONDS - Math.floor(elapsed));
13631
+ progressEtaRef.current.textContent = elapsed >= TARGET_SECONDS ? t("Finalizing...") : `~${remaining}s`;
13632
+ }
13329
13633
  if (progressStatusRef.current) {
13330
13634
  const status = [...statuses].reverse().find((s) => val >= s.at);
13331
- if (status) progressStatusRef.current.textContent = status.text;
13635
+ if (status && status.text !== progressLastStageRef.current) {
13636
+ const el = progressStatusRef.current;
13637
+ el.style.opacity = "0";
13638
+ setTimeout(() => {
13639
+ el.textContent = status.text;
13640
+ el.style.opacity = "1";
13641
+ }, 180);
13642
+ progressLastStageRef.current = status.text;
13643
+ }
13332
13644
  }
13333
13645
  }, 200);
13334
13646
  return () => {
@@ -13340,8 +13652,9 @@ function PrimeStyleTryonInner({
13340
13652
  clearInterval(progressIntervalRef.current);
13341
13653
  progressIntervalRef.current = null;
13342
13654
  }
13655
+ progressStartTsRef.current = null;
13343
13656
  }
13344
- }, [view]);
13657
+ }, [view, t]);
13345
13658
  useEffect(() => {
13346
13659
  return () => {
13347
13660
  if (previewUrl) URL.revokeObjectURL(previewUrl);
@@ -13668,7 +13981,16 @@ function PrimeStyleTryonInner({
13668
13981
  progressRef.current = 100;
13669
13982
  if (progressBarRef.current) progressBarRef.current.style.width = "100%";
13670
13983
  if (progressTextRef.current) progressTextRef.current.textContent = "100%";
13671
- if (progressStatusRef.current) progressStatusRef.current.textContent = t("Complete!");
13984
+ if (progressRingRef.current) progressRingRef.current.style.strokeDashoffset = "0";
13985
+ if (progressEtaRef.current) progressEtaRef.current.textContent = t("Done");
13986
+ if (progressStatusRef.current) {
13987
+ const el = progressStatusRef.current;
13988
+ el.style.opacity = "0";
13989
+ setTimeout(() => {
13990
+ el.textContent = t("Complete!");
13991
+ el.style.opacity = "1";
13992
+ }, 180);
13993
+ }
13672
13994
  cleanupJob();
13673
13995
  setTryOnProcessing(false);
13674
13996
  onComplete?.({ jobId: update.galleryId, imageUrl: update.imageUrl });
@@ -14077,25 +14399,29 @@ function PrimeStyleTryonInner({
14077
14399
  }
14078
14400
  completedRef.current = false;
14079
14401
  setTryOnProcessing(true);
14402
+ const vtoCategory = measurementTypeToVtoCategory(detectMeasurementType(productTitle));
14403
+ const isApparel = vtoCategory === "apparel";
14080
14404
  const previewObjUrl = (overrideFile ? null : previewUrl) || URL.createObjectURL(file);
14081
14405
  if (overrideFile || !previewUrl) setPreviewUrl(previewObjUrl);
14082
14406
  modelPoseRef.current = null;
14083
14407
  setBodyLandmarks(null);
14084
- detectMeasurementLines(previewObjUrl).then((lines) => {
14085
- modelPoseRef.current = lines;
14086
- }).catch(() => {
14087
- });
14088
- detectBodyLandmarks(previewObjUrl).then((lm) => {
14089
- setBodyLandmarks(lm);
14090
- }).catch(() => {
14091
- });
14408
+ if (isApparel) {
14409
+ detectMeasurementLines(previewObjUrl).then((lines) => {
14410
+ modelPoseRef.current = lines;
14411
+ }).catch(() => {
14412
+ });
14413
+ detectBodyLandmarks(previewObjUrl).then((lm) => {
14414
+ setBodyLandmarks(lm);
14415
+ }).catch(() => {
14416
+ });
14417
+ }
14092
14418
  try {
14093
14419
  const modelImage = await compressImage(file);
14094
14420
  let fitInfo;
14095
- if (sizingResult?.matchDetails?.length) {
14421
+ if (isApparel && sizingResult?.matchDetails?.length) {
14096
14422
  fitInfo = buildFitInfo(sizingResult.matchDetails, modelPoseRef.current);
14097
14423
  }
14098
- const response = await apiRef.current.submitTryOn(modelImage, productImage, fitInfo);
14424
+ const response = await apiRef.current.submitTryOn(modelImage, productImage, fitInfo, vtoCategory ?? "apparel");
14099
14425
  onProcessing?.(response.jobId);
14100
14426
  unsubRef.current = sseRef.current.onJob(response.jobId, handleVtoUpdate);
14101
14427
  let attempts = 0;
@@ -14126,11 +14452,13 @@ function PrimeStyleTryonInner({
14126
14452
  setView("error");
14127
14453
  onError?.({ message, code });
14128
14454
  }
14129
- }, [selectedFile, productImage, sizingResult, onProcessing, onError, handleVtoUpdate]);
14455
+ }, [selectedFile, productImage, productTitle, sizingResult, onProcessing, onError, handleVtoUpdate]);
14130
14456
  const handleRetryWithFit = useCallback(async (fitInfo) => {
14131
14457
  if (!selectedFile || !apiRef.current || !sseRef.current) return;
14132
14458
  setRetryLoading(true);
14133
- if (modelPoseRef.current) {
14459
+ const vtoCategory = measurementTypeToVtoCategory(detectMeasurementType(productTitle));
14460
+ const isApparel = vtoCategory === "apparel";
14461
+ if (isApparel && modelPoseRef.current) {
14134
14462
  const AREA_MAP = {
14135
14463
  chest: "chest",
14136
14464
  bust: "chest",
@@ -14156,7 +14484,8 @@ function PrimeStyleTryonInner({
14156
14484
  pollingRef.current = null;
14157
14485
  }
14158
14486
  const modelImage = await compressImage(selectedFile);
14159
- const response = await apiRef.current.submitTryOn(modelImage, productImage, fitInfo);
14487
+ const outboundFitInfo = isApparel ? fitInfo : void 0;
14488
+ const response = await apiRef.current.submitTryOn(modelImage, productImage, outboundFitInfo, vtoCategory ?? "apparel");
14160
14489
  unsubRef.current = sseRef.current.onJob(response.jobId, (update) => {
14161
14490
  if (update.status === "completed" && update.imageUrl) {
14162
14491
  setResultImageUrl(update.imageUrl);
@@ -14221,7 +14550,7 @@ function PrimeStyleTryonInner({
14221
14550
  } catch {
14222
14551
  setRetryLoading(false);
14223
14552
  }
14224
- }, [selectedFile, productImage]);
14553
+ }, [selectedFile, productImage, productTitle]);
14225
14554
  const handleDownload = useCallback(() => {
14226
14555
  if (!resultImageUrl) return;
14227
14556
  if (resultImageUrl.startsWith("data:")) {
@@ -14663,6 +14992,8 @@ function PrimeStyleTryonInner({
14663
14992
  progressBarRef,
14664
14993
  progressTextRef,
14665
14994
  progressStatusRef,
14995
+ progressEtaRef,
14996
+ progressRingRef,
14666
14997
  cn,
14667
14998
  t
14668
14999
  }