@primestyleai/tryon 2.3.0 → 3.1.0

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
  import { type CSSProperties } from "react";
2
- import type { ButtonStyles, ModalStyles, PrimeStyleClassNames, SizeGuideData } from "../types";
2
+ import type { ButtonStyles, ModalStyles, PrimeStyleClassNames } from "../types";
3
3
  export interface PrimeStyleTryonProps {
4
4
  productImage: string;
5
5
  productTitle?: string;
@@ -23,27 +23,7 @@ export interface PrimeStyleTryonProps {
23
23
  message: string;
24
24
  code?: string;
25
25
  }) => void;
26
- /** Pre-computed size guide — skips AI extraction if provided */
27
- sizeGuide?: SizeGuideData;
28
- /** Product description HTML for AI size guide extraction */
29
- productDescription?: string;
30
- /** Product vendor/brand for AI extraction */
31
- productVendor?: string;
32
- /** Product type (e.g. "T-Shirt", "Jacket") */
33
- productType?: string;
34
- /** Product tags for AI extraction */
35
- productTags?: string[];
36
- /** Product variants with size options */
37
- productVariants?: Array<{
38
- title: string;
39
- option1?: string | null;
40
- option2?: string | null;
41
- option3?: string | null;
42
- }>;
43
- /** Product options (e.g. [{name: "Size", values: ["S","M","L"]}]) */
44
- productOptions?: Array<{
45
- name: string;
46
- values?: string[];
47
- }>;
26
+ /** Size guide data from your backend pass as-is, any format (HTML, JSON, object, string). Our AI will parse it. */
27
+ sizeGuideData?: unknown;
48
28
  }
49
29
  export declare function PrimeStyleTryon(props: PrimeStyleTryonProps): import("react/jsx-runtime").JSX.Element | null;
@@ -179,13 +179,7 @@ function PrimeStyleTryonInner({
179
179
  onProcessing,
180
180
  onComplete,
181
181
  onError,
182
- sizeGuide: sizeGuideProp,
183
- productDescription,
184
- productVendor,
185
- productType,
186
- productTags,
187
- productVariants,
188
- productOptions
182
+ sizeGuideData
189
183
  }) {
190
184
  const [view, setView] = useState("idle");
191
185
  const [selectedFile, setSelectedFile] = useState(null);
@@ -193,9 +187,6 @@ function PrimeStyleTryonInner({
193
187
  const [resultImageUrl, setResultImageUrl] = useState(null);
194
188
  const [errorMessage, setErrorMessage] = useState(null);
195
189
  const [dragOver, setDragOver] = useState(false);
196
- const countdownRef = useRef(25);
197
- const countdownElRef = useRef(null);
198
- const countdownCircleRef = useRef(null);
199
190
  const [sizingMethod, setSizingMethod] = useState(null);
200
191
  const [sizingResult, setSizingResult] = useState(null);
201
192
  const [sizeGuide, setSizeGuide] = useState(null);
@@ -208,6 +199,7 @@ function PrimeStyleTryonInner({
208
199
  const [weightUnit, setWeightUnit] = useState(imperial ? "lbs" : "kg");
209
200
  const formRef = useRef({});
210
201
  const [formGender, setFormGender] = useState("male");
202
+ const [formKey, setFormKey] = useState(0);
211
203
  const [profiles, setProfiles] = useState(() => lsGet("profiles", []));
212
204
  const [history, setHistory] = useState(() => lsGet("history", []));
213
205
  const [activeProfileId, setActiveProfileId] = useState(null);
@@ -246,15 +238,33 @@ function PrimeStyleTryonInner({
246
238
  if (pollingRef.current) clearInterval(pollingRef.current);
247
239
  };
248
240
  }, [apiUrl]);
241
+ const progressRef = useRef(0);
242
+ const progressBarRef = useRef(null);
243
+ const progressTextRef = useRef(null);
244
+ const progressStatusRef = useRef(null);
249
245
  useEffect(() => {
250
246
  if (view === "processing") {
251
- countdownRef.current = 25;
247
+ progressRef.current = 0;
248
+ const statuses = [
249
+ { at: 0, text: "Preparing your image..." },
250
+ { at: 15, text: "Analyzing body proportions..." },
251
+ { at: 30, text: "Matching garment to your photo..." },
252
+ { at: 50, text: "Generating virtual try-on..." },
253
+ { at: 75, text: "Refining details..." },
254
+ { at: 90, text: "Almost there..." }
255
+ ];
252
256
  const interval = setInterval(() => {
253
- countdownRef.current -= 1;
254
- if (countdownElRef.current) countdownElRef.current.textContent = String(Math.max(countdownRef.current, 0));
255
- if (countdownCircleRef.current) countdownCircleRef.current.style.strokeDashoffset = `${Math.max(countdownRef.current, 0) / 25 * 326.73}`;
256
- if (countdownRef.current <= 0) clearInterval(interval);
257
- }, 1e3);
257
+ const p = progressRef.current;
258
+ const increment = p < 30 ? 1.2 : p < 60 ? 0.8 : p < 80 ? 0.4 : p < 95 ? 0.15 : 0;
259
+ progressRef.current = Math.min(p + increment, 95);
260
+ const val = Math.round(progressRef.current);
261
+ if (progressBarRef.current) progressBarRef.current.style.width = `${val}%`;
262
+ if (progressTextRef.current) progressTextRef.current.textContent = `${val}%`;
263
+ if (progressStatusRef.current) {
264
+ const status = [...statuses].reverse().find((s) => val >= s.at);
265
+ if (status) progressStatusRef.current.textContent = status.text;
266
+ }
267
+ }, 200);
258
268
  return () => clearInterval(interval);
259
269
  }
260
270
  }, [view]);
@@ -290,31 +300,22 @@ function PrimeStyleTryonInner({
290
300
  useEffect(() => {
291
301
  if (view !== "sizing-choice" || sizeGuideFetchedRef.current || !apiRef.current) return;
292
302
  sizeGuideFetchedRef.current = true;
293
- if (sizeGuideProp) {
294
- setSizeGuide(sizeGuideProp);
303
+ if (!sizeGuideData) {
304
+ setSizeGuide({ found: false });
295
305
  return;
296
306
  }
297
307
  setSizeGuideFetching(true);
298
308
  const baseUrl = getApiUrl(apiUrl);
299
309
  const key = getApiKey();
300
- const productPayload = {
301
- title: productTitle,
302
- variants: productVariants || []
303
- };
304
- if (productDescription) productPayload.description = productDescription;
305
- if (productVendor) productPayload.vendor = productVendor;
306
- if (productType) productPayload.productType = productType;
307
- if (productTags?.length) productPayload.tags = productTags;
308
- if (productOptions?.length) productPayload.options = productOptions;
309
310
  fetch(`${baseUrl}/api/v1/sizing/sizeguide`, {
310
311
  method: "POST",
311
312
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
312
- body: JSON.stringify({ product: productPayload })
313
+ body: JSON.stringify({ product: { title: productTitle }, sizeGuideRaw: sizeGuideData })
313
314
  }).then((r) => r.ok ? r.json() : null).then((data) => {
314
315
  if (data) setSizeGuide(data);
315
316
  else setSizeGuide({ found: false });
316
317
  }).catch(() => setSizeGuide({ found: false })).finally(() => setSizeGuideFetching(false));
317
- }, [view, apiUrl, productTitle, sizeGuideProp]);
318
+ }, [view, apiUrl, productTitle, sizeGuideData]);
318
319
  const stepIndex = useMemo(() => {
319
320
  switch (view) {
320
321
  case "welcome":
@@ -400,8 +401,12 @@ function PrimeStyleTryonInner({
400
401
  });
401
402
  if (!completedRef.current) {
402
403
  completedRef.current = true;
404
+ progressRef.current = 100;
405
+ if (progressBarRef.current) progressBarRef.current.style.width = "100%";
406
+ if (progressTextRef.current) progressTextRef.current.textContent = "100%";
407
+ if (progressStatusRef.current) progressStatusRef.current.textContent = "Complete!";
403
408
  cleanupJob();
404
- setView("result");
409
+ setTimeout(() => setView("result"), 400);
405
410
  onComplete?.({ jobId: update.galleryId, imageUrl: update.imageUrl });
406
411
  }
407
412
  } else if (update.status === "failed") {
@@ -422,14 +427,7 @@ function PrimeStyleTryonInner({
422
427
  const payload = {
423
428
  method: sizingMethod,
424
429
  locale: sizingCountry,
425
- product: {
426
- title: productTitle,
427
- description: productDescription || "",
428
- variants: productVariants || [],
429
- ...productVendor && { vendor: productVendor },
430
- ...productType && { productType },
431
- ...productTags?.length && { tags: productTags }
432
- }
430
+ product: { title: productTitle, description: "", variants: [] }
433
431
  };
434
432
  if (sizeGuide?.found) payload.sizeGuide = sizeGuide;
435
433
  if (sizingMethod === "exact") {
@@ -563,6 +561,11 @@ function PrimeStyleTryonInner({
563
561
  if (p.fitPreference) fd.fitPreference = p.fitPreference;
564
562
  formRef.current = fd;
565
563
  setFormGender(fd.gender || "male");
564
+ if (p.country) setSizingCountry(p.country);
565
+ if (p.sizingUnit) setSizingUnit(p.sizingUnit);
566
+ if (p.heightUnit) setHeightUnit(p.heightUnit);
567
+ if (p.weightUnit) setWeightUnit(p.weightUnit);
568
+ setFormKey((k) => k + 1);
566
569
  }, [profiles]);
567
570
  const saveProfile = useCallback((name) => {
568
571
  const id = activeProfileId || `p_${Date.now()}`;
@@ -585,6 +588,10 @@ function PrimeStyleTryonInner({
585
588
  shoeUS: formRef.current.shoeUS,
586
589
  shoeUK: formRef.current.shoeUK,
587
590
  fitPreference: formRef.current.fitPreference,
591
+ country: sizingCountry,
592
+ sizingUnit,
593
+ heightUnit,
594
+ weightUnit,
588
595
  createdAt: Date.now()
589
596
  };
590
597
  setProfiles((prev) => {
@@ -598,7 +605,7 @@ function PrimeStyleTryonInner({
598
605
  });
599
606
  setActiveProfileId(id);
600
607
  setProfileSaved(true);
601
- }, [activeProfileId]);
608
+ }, [activeProfileId, sizingCountry, sizingUnit, heightUnit, weightUnit]);
602
609
  const saveHistoryEntry = useCallback(() => {
603
610
  const entry = {
604
611
  id: `h_${Date.now()}`,
@@ -849,7 +856,11 @@ function PrimeStyleTryonInner({
849
856
  /* @__PURE__ */ jsx("label", { children: "Sizing region" }),
850
857
  /* @__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)) })
851
858
  ] }),
852
- sizingMethod === "exact" && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "cm", value: "cm" }, { label: "in", value: "in" }], value: sizingUnit, onChange: setSizingUnit }) }),
859
+ sizingMethod === "exact" && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "cm", value: "cm" }, { label: "in", value: "in" }], value: sizingUnit, onChange: (v) => {
860
+ setSizingUnit(v);
861
+ setHeightUnit(v === "cm" ? "cm" : "ft");
862
+ setWeightUnit(v === "cm" ? "kg" : "lbs");
863
+ } }) }),
853
864
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
854
865
  /* @__PURE__ */ jsx("label", { children: "Height" }),
855
866
  heightUnit === "ft" ? /* @__PURE__ */ jsxs("div", { className: "ps-tryon-height-ft", children: [
@@ -905,18 +916,20 @@ function PrimeStyleTryonInner({
905
916
  "Get My Size & Try On ",
906
917
  /* @__PURE__ */ jsx(ArrowRightIcon, {})
907
918
  ] })
908
- ] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}`);
919
+ ] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}-${formKey}`);
909
920
  }
910
921
  function ProcessingView() {
911
922
  return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
912
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-countdown-ring", children: [
913
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 120 120", children: [
914
- /* @__PURE__ */ jsx("circle", { className: "ps-tryon-countdown-track", cx: "60", cy: "60", r: "52" }),
915
- /* @__PURE__ */ jsx("circle", { ref: countdownCircleRef, className: "ps-tryon-countdown-progress", cx: "60", cy: "60", r: "52", style: { strokeDashoffset: `${25 / 25 * 326.73}` } })
916
- ] }),
917
- /* @__PURE__ */ jsx("span", { ref: countdownElRef, className: "ps-tryon-countdown-number", children: "25" })
923
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing-image-wrap", children: [
924
+ previewUrl && /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "Your photo", className: "ps-tryon-processing-model" }),
925
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-line" }),
926
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-overlay" })
918
927
  ] }),
919
- /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-text", cn.processingText), children: "Generating your try-on..." }),
928
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-section", children: [
929
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-progress-bar-wrap", children: /* @__PURE__ */ jsx("div", { ref: progressBarRef, className: "ps-tryon-progress-bar-fill", style: { width: "0%" } }) }),
930
+ /* @__PURE__ */ jsx("span", { ref: progressTextRef, className: "ps-tryon-progress-pct", children: "0%" })
931
+ ] }),
932
+ /* @__PURE__ */ jsx("div", { ref: progressStatusRef, className: cx("ps-tryon-processing-text", cn.processingText), children: "Preparing your image..." }),
920
933
  /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-sub", cn.processingSubText), children: "This usually takes 15-25 seconds" })
921
934
  ] });
922
935
  }
@@ -1346,19 +1359,49 @@ const STYLES = `
1346
1359
  .ps-tryon-disclaimer { font-size: 11px; color: #666; margin: 4px 0 0; }
1347
1360
 
1348
1361
  /* Processing */
1349
- .ps-tryon-processing { text-align: center; padding: 40px 24px; }
1350
- .ps-tryon-spinner {
1351
- width: 48px; height: 48px; border: 3px solid #333;
1352
- border-top-color: var(--ps-loader, #bb945c); border-radius: 50%;
1353
- animation: ps-spin 0.8s linear infinite; margin: 0 auto 16px;
1362
+ .ps-tryon-processing { text-align: center; padding: 24px; display: flex; flex-direction: column; align-items: center; }
1363
+
1364
+ .ps-tryon-processing-image-wrap {
1365
+ position: relative; width: 200px; height: 260px; margin: 0 auto 24px;
1366
+ border-radius: 16px; overflow: hidden; border: 2px solid #333;
1367
+ }
1368
+ .ps-tryon-processing-model {
1369
+ width: 100%; height: 100%; object-fit: cover; display: block;
1370
+ }
1371
+ .ps-tryon-scan-overlay {
1372
+ position: absolute; inset: 0;
1373
+ background: linear-gradient(180deg, rgba(187,148,92,0.05) 0%, transparent 40%, transparent 60%, rgba(187,148,92,0.05) 100%);
1374
+ pointer-events: none;
1354
1375
  }
1355
- @keyframes ps-spin { to { transform: rotate(360deg); } }
1356
- .ps-tryon-countdown-ring { position: relative; width: 100px; height: 100px; margin: 0 auto 20px; }
1357
- .ps-tryon-countdown-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
1358
- .ps-tryon-countdown-track { fill: none; stroke: #333; stroke-width: 4; }
1359
- .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; }
1360
- .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; }
1361
- .ps-tryon-done-pulse { animation: ps-scale-in 0.4s ease both; }
1376
+ .ps-tryon-scan-line {
1377
+ position: absolute; left: 0; right: 0; height: 3px;
1378
+ background: linear-gradient(90deg, transparent, #bb945c 20%, #d6ba7d 50%, #bb945c 80%, transparent);
1379
+ box-shadow: 0 0 15px rgba(187,148,92,0.6), 0 0 40px rgba(187,148,92,0.2);
1380
+ animation: ps-scan 2.5s ease-in-out infinite;
1381
+ pointer-events: none; z-index: 2;
1382
+ }
1383
+ @keyframes ps-scan {
1384
+ 0% { top: 0; opacity: 0; }
1385
+ 5% { opacity: 1; }
1386
+ 95% { opacity: 1; }
1387
+ 100% { top: calc(100% - 3px); opacity: 0; }
1388
+ }
1389
+
1390
+ .ps-tryon-progress-section {
1391
+ display: flex; align-items: center; gap: 12px; width: 100%; max-width: 300px; margin-bottom: 16px;
1392
+ }
1393
+ .ps-tryon-progress-bar-wrap {
1394
+ flex: 1; height: 6px; background: #333; border-radius: 3px; overflow: hidden;
1395
+ }
1396
+ .ps-tryon-progress-bar-fill {
1397
+ height: 100%; background: linear-gradient(90deg, #bb945c, #d6ba7d);
1398
+ border-radius: 3px; transition: width 0.3s ease;
1399
+ }
1400
+ .ps-tryon-progress-pct {
1401
+ font-size: 13px; font-weight: 700; color: #bb945c; min-width: 36px; text-align: right;
1402
+ font-variant-numeric: tabular-nums;
1403
+ }
1404
+
1362
1405
  @keyframes ps-scale-in { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } }
1363
1406
  .ps-tryon-processing-text { font-size: 14px; color: #fff; margin: 0 0 4px; }
1364
1407
  .ps-tryon-processing-sub { font-size: 12px; color: #999; margin: 0; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "2.3.0",
3
+ "version": "3.1.0",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",