@primestyleai/tryon 2.0.1 → 2.0.3

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 +177 -134
  2. package/package.json +1 -1
@@ -60,6 +60,22 @@ function lbsToKg(lbs) {
60
60
  function ftInToCm(ft, inch) {
61
61
  return +(ft * 30.48 + inch * 2.54).toFixed(1);
62
62
  }
63
+ function SvgIcon({ d, size = 18, strokeWidth = 2 }) {
64
+ return /* @__PURE__ */ jsx(
65
+ "svg",
66
+ {
67
+ width: size,
68
+ height: size,
69
+ viewBox: "0 0 24 24",
70
+ fill: "none",
71
+ stroke: "currentColor",
72
+ strokeWidth,
73
+ strokeLinecap: "round",
74
+ strokeLinejoin: "round",
75
+ children: /* @__PURE__ */ jsx("path", { d })
76
+ }
77
+ );
78
+ }
63
79
  function CameraIcon({ size = 18 }) {
64
80
  return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
65
81
  /* @__PURE__ */ jsx("path", { d: "M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" }),
@@ -160,7 +176,9 @@ function PrimeStyleTryon({
160
176
  const [resultImageUrl, setResultImageUrl] = useState(null);
161
177
  const [errorMessage, setErrorMessage] = useState(null);
162
178
  const [dragOver, setDragOver] = useState(false);
163
- const [countdown, setCountdown] = useState(25);
179
+ const countdownRef = useRef(25);
180
+ const countdownElRef = useRef(null);
181
+ const countdownCircleRef = useRef(null);
164
182
  const [sizingMethod, setSizingMethod] = useState(null);
165
183
  const [sizingResult, setSizingResult] = useState(null);
166
184
  const [sizeGuide, setSizeGuide] = useState(null);
@@ -169,7 +187,8 @@ function PrimeStyleTryon({
169
187
  const [sizingUnit, setSizingUnit] = useState(imperial ? "in" : "cm");
170
188
  const [heightUnit, setHeightUnit] = useState(imperial ? "ft" : "cm");
171
189
  const [weightUnit, setWeightUnit] = useState(imperial ? "lbs" : "kg");
172
- const [formData, setFormData] = useState({});
190
+ const formRef = useRef({});
191
+ const [formGender, setFormGender] = useState("male");
173
192
  const [profiles, setProfiles] = useState(() => lsGet("profiles", []));
174
193
  const [history, setHistory] = useState(() => lsGet("history", []));
175
194
  const [activeProfileId, setActiveProfileId] = useState(null);
@@ -199,15 +218,12 @@ function PrimeStyleTryon({
199
218
  }, [apiUrl]);
200
219
  useEffect(() => {
201
220
  if (view === "processing") {
202
- setCountdown(25);
221
+ countdownRef.current = 25;
203
222
  const interval = setInterval(() => {
204
- setCountdown((p) => {
205
- if (p <= 1) {
206
- clearInterval(interval);
207
- return 0;
208
- }
209
- return p - 1;
210
- });
223
+ countdownRef.current -= 1;
224
+ if (countdownElRef.current) countdownElRef.current.textContent = String(Math.max(countdownRef.current, 0));
225
+ if (countdownCircleRef.current) countdownCircleRef.current.style.strokeDashoffset = `${Math.max(countdownRef.current, 0) / 25 * 326.73}`;
226
+ if (countdownRef.current <= 0) clearInterval(interval);
211
227
  }, 1e3);
212
228
  return () => clearInterval(interval);
213
229
  }
@@ -275,7 +291,8 @@ function PrimeStyleTryon({
275
291
  setSizingResult(null);
276
292
  setSizeGuide(null);
277
293
  setProfileSaved(false);
278
- setFormData({});
294
+ formRef.current = {};
295
+ setFormGender("male");
279
296
  unsubRef.current?.();
280
297
  unsubRef.current = null;
281
298
  if (pollingRef.current) {
@@ -349,23 +366,23 @@ function PrimeStyleTryon({
349
366
  };
350
367
  if (sizeGuide?.found) payload.sizeGuide = sizeGuide;
351
368
  if (sizingMethod === "exact") {
352
- const m = { gender: formData.gender || "male" };
353
- if (formData.height) m.heightCm = heightUnit === "ft" ? ftInToCm(parseFloat(formData.heightFeet || "0"), parseFloat(formData.heightInches || "0")) : parseFloat(formData.height);
354
- if (formData.weight) m.weightKg = weightUnit === "lbs" ? lbsToKg(parseFloat(formData.weight)) : parseFloat(formData.weight);
369
+ const m = { gender: formRef.current.gender || "male" };
370
+ if (formRef.current.height) m.heightCm = heightUnit === "ft" ? ftInToCm(parseFloat(formRef.current.heightFeet || "0"), parseFloat(formRef.current.heightInches || "0")) : parseFloat(formRef.current.height);
371
+ if (formRef.current.weight) m.weightKg = weightUnit === "lbs" ? lbsToKg(parseFloat(formRef.current.weight)) : parseFloat(formRef.current.weight);
355
372
  const keys = ["chest", "bust", "waist", "hips", "shoulderWidth", "sleeveLength", "inseam", "neckCircumference", "footLengthCm"];
356
373
  for (const k of keys) {
357
- if (formData[k]) m[k] = sizingUnit === "in" ? inToCm(parseFloat(formData[k])) : parseFloat(formData[k]);
374
+ if (formRef.current[k]) m[k] = sizingUnit === "in" ? inToCm(parseFloat(formRef.current[k])) : parseFloat(formRef.current[k]);
358
375
  }
359
- if (formData.shoeEU) m.shoeEU = formData.shoeEU;
360
- if (formData.shoeUS) m.shoeUS = formData.shoeUS;
361
- if (formData.shoeUK) m.shoeUK = formData.shoeUK;
362
- if (formData.fitPreference) m.fitPreference = formData.fitPreference;
376
+ if (formRef.current.shoeEU) m.shoeEU = formRef.current.shoeEU;
377
+ if (formRef.current.shoeUS) m.shoeUS = formRef.current.shoeUS;
378
+ if (formRef.current.shoeUK) m.shoeUK = formRef.current.shoeUK;
379
+ if (formRef.current.fitPreference) m.fitPreference = formRef.current.fitPreference;
363
380
  payload.measurements = m;
364
381
  } else {
365
382
  payload.quickEstimate = {
366
- heightCm: heightUnit === "ft" ? ftInToCm(parseFloat(formData.heightFeet || "0"), parseFloat(formData.heightInches || "0")) : parseFloat(formData.height || "0"),
367
- weightKg: weightUnit === "lbs" ? lbsToKg(parseFloat(formData.weight || "0")) : parseFloat(formData.weight || "0"),
368
- gender: formData.gender || "male"
383
+ heightCm: heightUnit === "ft" ? ftInToCm(parseFloat(formRef.current.heightFeet || "0"), parseFloat(formRef.current.heightInches || "0")) : parseFloat(formRef.current.height || "0"),
384
+ weightKg: weightUnit === "lbs" ? lbsToKg(parseFloat(formRef.current.weight || "0")) : parseFloat(formRef.current.weight || "0"),
385
+ gender: formRef.current.gender || "male"
369
386
  };
370
387
  }
371
388
  try {
@@ -380,7 +397,7 @@ function PrimeStyleTryon({
380
397
  }
381
398
  } catch {
382
399
  }
383
- }, [apiUrl, sizingMethod, sizingCountry, formData, heightUnit, weightUnit, sizingUnit, sizeGuide, productTitle]);
400
+ }, [apiUrl, sizingMethod, sizingCountry, heightUnit, weightUnit, sizingUnit, sizeGuide, productTitle]);
384
401
  const handleSubmit = useCallback(async () => {
385
402
  if (!selectedFile || !apiRef.current || !sseRef.current) {
386
403
  const msg = !apiRef.current ? "Missing NEXT_PUBLIC_PRIMESTYLE_API_KEY" : "No file selected";
@@ -477,29 +494,30 @@ function PrimeStyleTryon({
477
494
  if (p.shoeUS) fd.shoeUS = p.shoeUS;
478
495
  if (p.shoeUK) fd.shoeUK = p.shoeUK;
479
496
  if (p.fitPreference) fd.fitPreference = p.fitPreference;
480
- setFormData(fd);
497
+ formRef.current = fd;
498
+ setFormGender(fd.gender || "male");
481
499
  }, [profiles]);
482
500
  const saveProfile = useCallback((name) => {
483
501
  const id = activeProfileId || `p_${Date.now()}`;
484
502
  const p = {
485
503
  id,
486
504
  name,
487
- gender: formData.gender || "male",
488
- heightCm: formData.height ? parseFloat(formData.height) : void 0,
489
- weightKg: formData.weight ? parseFloat(formData.weight) : void 0,
490
- chest: formData.chest ? parseFloat(formData.chest) : void 0,
491
- bust: formData.bust ? parseFloat(formData.bust) : void 0,
492
- waist: formData.waist ? parseFloat(formData.waist) : void 0,
493
- hips: formData.hips ? parseFloat(formData.hips) : void 0,
494
- shoulderWidth: formData.shoulderWidth ? parseFloat(formData.shoulderWidth) : void 0,
495
- sleeveLength: formData.sleeveLength ? parseFloat(formData.sleeveLength) : void 0,
496
- inseam: formData.inseam ? parseFloat(formData.inseam) : void 0,
497
- neckCircumference: formData.neckCircumference ? parseFloat(formData.neckCircumference) : void 0,
498
- footLengthCm: formData.footLengthCm ? parseFloat(formData.footLengthCm) : void 0,
499
- shoeEU: formData.shoeEU,
500
- shoeUS: formData.shoeUS,
501
- shoeUK: formData.shoeUK,
502
- fitPreference: formData.fitPreference,
505
+ gender: formRef.current.gender || "male",
506
+ heightCm: formRef.current.height ? parseFloat(formRef.current.height) : void 0,
507
+ weightKg: formRef.current.weight ? parseFloat(formRef.current.weight) : void 0,
508
+ chest: formRef.current.chest ? parseFloat(formRef.current.chest) : void 0,
509
+ bust: formRef.current.bust ? parseFloat(formRef.current.bust) : void 0,
510
+ waist: formRef.current.waist ? parseFloat(formRef.current.waist) : void 0,
511
+ hips: formRef.current.hips ? parseFloat(formRef.current.hips) : void 0,
512
+ shoulderWidth: formRef.current.shoulderWidth ? parseFloat(formRef.current.shoulderWidth) : void 0,
513
+ sleeveLength: formRef.current.sleeveLength ? parseFloat(formRef.current.sleeveLength) : void 0,
514
+ inseam: formRef.current.inseam ? parseFloat(formRef.current.inseam) : void 0,
515
+ neckCircumference: formRef.current.neckCircumference ? parseFloat(formRef.current.neckCircumference) : void 0,
516
+ footLengthCm: formRef.current.footLengthCm ? parseFloat(formRef.current.footLengthCm) : void 0,
517
+ shoeEU: formRef.current.shoeEU,
518
+ shoeUS: formRef.current.shoeUS,
519
+ shoeUK: formRef.current.shoeUK,
520
+ fitPreference: formRef.current.fitPreference,
503
521
  createdAt: Date.now()
504
522
  };
505
523
  setProfiles((prev) => {
@@ -513,7 +531,7 @@ function PrimeStyleTryon({
513
531
  });
514
532
  setActiveProfileId(id);
515
533
  setProfileSaved(true);
516
- }, [activeProfileId, formData]);
534
+ }, [activeProfileId]);
517
535
  const saveHistoryEntry = useCallback(() => {
518
536
  const entry = {
519
537
  id: `h_${Date.now()}`,
@@ -539,7 +557,7 @@ function PrimeStyleTryon({
539
557
  }
540
558
  }, [view]);
541
559
  const updateField = useCallback((key, val) => {
542
- setFormData((prev) => ({ ...prev, [key]: val }));
560
+ formRef.current[key] = val;
543
561
  }, []);
544
562
  const shoeField = useMemo(() => {
545
563
  const map = {
@@ -604,8 +622,8 @@ function PrimeStyleTryon({
604
622
  {
605
623
  type,
606
624
  placeholder,
607
- value: formData[fieldKey] || "",
608
- onChange: (e) => updateField(fieldKey, e.target.value)
625
+ defaultValue: formRef.current[fieldKey] || "",
626
+ onInput: (e) => updateField(fieldKey, e.target.value)
609
627
  }
610
628
  ),
611
629
  unit && /* @__PURE__ */ jsx("span", { className: "ps-tryon-input-unit", children: unit })
@@ -709,76 +727,92 @@ function PrimeStyleTryon({
709
727
  setView("sizing-form");
710
728
  }, children: [
711
729
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-icon", children: /* @__PURE__ */ jsx(RulerIcon, { size: 24 }) }),
712
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-title", children: "Exact Measurements" }),
713
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-desc", children: "Enter your body measurements for the most accurate fit" }),
714
- /* @__PURE__ */ jsx("span", { className: "ps-tryon-choice-badge", children: "Most Accurate" })
730
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-choice-info", children: [
731
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-title", children: "Enter my measurements" }),
732
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-desc", children: "Chest, waist, hips, shoes & more" })
733
+ ] }),
734
+ /* @__PURE__ */ jsx("span", { className: "ps-tryon-choice-badge", children: "Best accuracy" })
715
735
  ] }),
716
736
  /* @__PURE__ */ jsxs("button", { className: "ps-tryon-choice-card", onClick: () => {
717
737
  setSizingMethod("quick");
718
738
  setView("sizing-form");
719
739
  }, children: [
720
740
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-icon", children: /* @__PURE__ */ jsx(SparkleIcon, { size: 24 }) }),
721
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-title", children: "Quick Estimate" }),
722
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-desc", children: "Just height, weight & gender — fast but less precise" })
741
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-choice-info", children: [
742
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-title", children: "Just height & weight" }),
743
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-desc", children: "Quick estimate in seconds" })
744
+ ] })
745
+ ] }),
746
+ /* @__PURE__ */ jsxs("button", { className: "ps-tryon-choice-card", onClick: () => {
747
+ setSizingMethod(null);
748
+ handleSubmit();
749
+ }, children: [
750
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-icon", children: /* @__PURE__ */ jsx(SvgIcon, { d: "M13 5H1M13 9H1M13 13H1M5 17l4 4 8-8", size: 24, strokeWidth: 1.5 }) }),
751
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-choice-info", children: [
752
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-title", children: "Skip sizing" }),
753
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-desc", children: "Just show me the try-on" })
754
+ ] })
723
755
  ] })
724
- ] }),
725
- /* @__PURE__ */ jsx("button", { className: "ps-tryon-skip-btn", onClick: () => {
726
- setSizingMethod(null);
727
- handleSubmit();
728
- }, children: "Skip sizing — just try it on" })
756
+ ] })
729
757
  ] });
730
758
  }
731
759
  function SizingFormView() {
732
- const isFemale = formData.gender === "female";
760
+ const isFemale = formGender === "female";
761
+ const isCm = sizingUnit === "cm";
733
762
  return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-sizing-form", children: [
734
763
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
735
- /* @__PURE__ */ jsx("label", { children: "Sizing region" }),
736
- /* @__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)) })
737
- ] }),
738
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
739
- /* @__PURE__ */ jsx("label", { children: "Gender" }),
764
+ /* @__PURE__ */ jsx("label", { children: "I'm shopping for" }),
740
765
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-unit-toggle", children: [
741
- /* @__PURE__ */ jsx("button", { className: `ps-tryon-unit-btn${formData.gender !== "female" ? " ps-active" : ""}`, onClick: () => updateField("gender", "male"), children: "Men's" }),
742
- /* @__PURE__ */ jsx("button", { className: `ps-tryon-unit-btn${formData.gender === "female" ? " ps-active" : ""}`, onClick: () => updateField("gender", "female"), children: "Women's" })
766
+ /* @__PURE__ */ jsx("button", { className: `ps-tryon-unit-btn${!isFemale ? " ps-active" : ""}`, onClick: () => {
767
+ updateField("gender", "male");
768
+ setFormGender("male");
769
+ }, children: "Men's" }),
770
+ /* @__PURE__ */ jsx("button", { className: `ps-tryon-unit-btn${isFemale ? " ps-active" : ""}`, onClick: () => {
771
+ updateField("gender", "female");
772
+ setFormGender("female");
773
+ }, children: "Women's" })
743
774
  ] })
744
775
  ] }),
745
776
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
746
- /* @__PURE__ */ jsx("label", { children: "Height" }),
747
- /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "cm", value: "cm" }, { label: "ft/in", value: "ft" }], value: heightUnit, onChange: setHeightUnit })
777
+ /* @__PURE__ */ jsx("label", { children: "Sizing region" }),
778
+ /* @__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)) })
748
779
  ] }),
749
- heightUnit === "ft" ? /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row ps-tryon-dual-input", children: [
750
- /* @__PURE__ */ jsx("input", { type: "number", placeholder: "ft", value: formData.heightFeet || "", onChange: (e) => updateField("heightFeet", e.target.value) }),
751
- /* @__PURE__ */ jsx("input", { type: "number", placeholder: "in", value: formData.heightInches || "", onChange: (e) => updateField("heightInches", e.target.value) })
752
- ] }) : /* @__PURE__ */ jsx(InputRow, { label: "", fieldKey: "height", placeholder: "e.g. 175", type: "number", unit: "cm" }),
780
+ sizingMethod === "exact" && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "cm", value: "cm" }, { label: "in", value: "in" }], value: sizingUnit, onChange: setSizingUnit }) }),
753
781
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
754
- /* @__PURE__ */ jsx("label", { children: "Weight" }),
755
- /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "kg", value: "kg" }, { label: "lbs", value: "lbs" }], value: weightUnit, onChange: setWeightUnit })
782
+ /* @__PURE__ */ jsx("label", { children: "Height" }),
783
+ heightUnit === "ft" ? /* @__PURE__ */ jsxs("div", { className: "ps-tryon-height-ft", children: [
784
+ /* @__PURE__ */ jsx("input", { type: "number", placeholder: "5", defaultValue: formRef.current.heightFeet || "", onInput: (e) => updateField("heightFeet", e.target.value) }),
785
+ /* @__PURE__ */ jsx("span", { children: "ft" }),
786
+ /* @__PURE__ */ jsx("input", { type: "number", placeholder: "4", defaultValue: formRef.current.heightInches || "", onInput: (e) => updateField("heightInches", e.target.value) }),
787
+ /* @__PURE__ */ jsx("span", { children: "in" })
788
+ ] }) : /* @__PURE__ */ jsx("input", { type: "number", placeholder: "e.g. 175", defaultValue: formRef.current.height || "", onInput: (e) => updateField("height", e.target.value) }),
789
+ /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "cm", value: "cm" }, { label: "ft", value: "ft" }], value: heightUnit, onChange: setHeightUnit })
756
790
  ] }),
757
- /* @__PURE__ */ jsx(InputRow, { label: "", fieldKey: "weight", placeholder: weightUnit === "lbs" ? "e.g. 170" : "e.g. 75", type: "number", unit: weightUnit }),
758
- sizingMethod === "exact" && /* @__PURE__ */ jsxs(Fragment, { children: [
759
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
760
- /* @__PURE__ */ jsx("label", { children: "Measurements" }),
761
- /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "cm", value: "cm" }, { label: "in", value: "in" }], value: sizingUnit, onChange: setSizingUnit })
762
- ] }),
791
+ sizingMethod === "exact" ? /* @__PURE__ */ jsxs(Fragment, { children: [
763
792
  isFemale ? /* @__PURE__ */ jsxs(Fragment, { children: [
764
- /* @__PURE__ */ jsx(InputRow, { label: "Bust", fieldKey: "bust", placeholder: "e.g. 90", type: "number", unit: sizingUnit }),
765
- /* @__PURE__ */ jsx(InputRow, { label: "Waist", fieldKey: "waist", placeholder: "e.g. 70", type: "number", unit: sizingUnit }),
766
- /* @__PURE__ */ jsx(InputRow, { label: "Hips", fieldKey: "hips", placeholder: "e.g. 95", type: "number", unit: sizingUnit })
793
+ /* @__PURE__ */ jsx(InputRow, { label: "Bust *", fieldKey: "bust", placeholder: isCm ? "e.g. 88" : "e.g. 35", type: "number", unit: sizingUnit }),
794
+ /* @__PURE__ */ jsx(InputRow, { label: "Waist *", fieldKey: "waist", placeholder: isCm ? "e.g. 70" : "e.g. 28", type: "number", unit: sizingUnit }),
795
+ /* @__PURE__ */ jsx(InputRow, { label: "Hips *", fieldKey: "hips", placeholder: isCm ? "e.g. 96" : "e.g. 38", type: "number", unit: sizingUnit }),
796
+ /* @__PURE__ */ jsx(InputRow, { label: "Shoulders", fieldKey: "shoulderWidth", placeholder: isCm ? "e.g. 39" : "e.g. 15", type: "number", unit: sizingUnit }),
797
+ /* @__PURE__ */ jsx(InputRow, { label: "Inseam", fieldKey: "inseam", placeholder: isCm ? "e.g. 76" : "e.g. 30", type: "number", unit: sizingUnit })
767
798
  ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
768
- /* @__PURE__ */ jsx(InputRow, { label: "Chest", fieldKey: "chest", placeholder: "e.g. 100", type: "number", unit: sizingUnit }),
769
- /* @__PURE__ */ jsx(InputRow, { label: "Waist", fieldKey: "waist", placeholder: "e.g. 82", type: "number", unit: sizingUnit }),
770
- /* @__PURE__ */ jsx(InputRow, { label: "Hips", fieldKey: "hips", placeholder: "e.g. 98", type: "number", unit: sizingUnit })
799
+ /* @__PURE__ */ jsx(InputRow, { label: "Chest *", fieldKey: "chest", placeholder: isCm ? "e.g. 104" : "e.g. 41", type: "number", unit: sizingUnit }),
800
+ /* @__PURE__ */ jsx(InputRow, { label: "Waist *", fieldKey: "waist", placeholder: isCm ? "e.g. 84" : "e.g. 33", type: "number", unit: sizingUnit }),
801
+ /* @__PURE__ */ jsx(InputRow, { label: "Shoulders", fieldKey: "shoulderWidth", placeholder: isCm ? "e.g. 46" : "e.g. 18", type: "number", unit: sizingUnit }),
802
+ /* @__PURE__ */ jsx(InputRow, { label: "Sleeve", fieldKey: "sleeveLength", placeholder: isCm ? "e.g. 64" : "e.g. 25", type: "number", unit: sizingUnit }),
803
+ /* @__PURE__ */ jsx(InputRow, { label: "Inseam", fieldKey: "inseam", placeholder: isCm ? "e.g. 81" : "e.g. 32", type: "number", unit: sizingUnit })
771
804
  ] }),
772
- /* @__PURE__ */ jsx(InputRow, { label: "Shoulder Width", fieldKey: "shoulderWidth", placeholder: "e.g. 45", type: "number", unit: sizingUnit }),
773
- /* @__PURE__ */ jsx(InputRow, { label: "Sleeve Length", fieldKey: "sleeveLength", placeholder: "e.g. 65", type: "number", unit: sizingUnit }),
774
- /* @__PURE__ */ jsx(InputRow, { label: "Inseam", fieldKey: "inseam", placeholder: "e.g. 80", type: "number", unit: sizingUnit }),
775
805
  isFemale && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
776
- /* @__PURE__ */ jsx("label", { children: "Fit preference" }),
806
+ /* @__PURE__ */ jsx("label", { children: "Fit type" }),
777
807
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-unit-toggle", children: ["petite", "standard", "tall", "plus"].map((fp) => /* @__PURE__ */ jsx(
778
808
  "button",
779
809
  {
780
- className: `ps-tryon-unit-btn${(formData.fitPreference || "standard") === fp ? " ps-active" : ""}`,
781
- onClick: () => updateField("fitPreference", fp),
810
+ className: `ps-tryon-unit-btn${(formRef.current.fitPreference || "standard") === fp ? " ps-active" : ""}`,
811
+ onClick: (e) => {
812
+ updateField("fitPreference", fp);
813
+ const btns = e.target.parentElement.querySelectorAll(".ps-tryon-unit-btn");
814
+ btns.forEach((b) => b.classList.toggle("ps-active", b.textContent?.toLowerCase() === fp));
815
+ },
782
816
  children: fp.charAt(0).toUpperCase() + fp.slice(1)
783
817
  },
784
818
  fp
@@ -786,28 +820,32 @@ function PrimeStyleTryon({
786
820
  ] }),
787
821
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-shoe-section", children: [
788
822
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-shoe-title", children: "Shoe sizing (optional)" }),
789
- /* @__PURE__ */ jsx(InputRow, { label: "Foot length", fieldKey: "footLengthCm", placeholder: sizingUnit === "in" ? "e.g. 10.5" : "e.g. 27", type: "number", unit: sizingUnit }),
823
+ /* @__PURE__ */ jsx(InputRow, { label: "Foot length", fieldKey: "footLengthCm", placeholder: isCm ? "e.g. 27" : "e.g. 10.5", type: "number", unit: sizingUnit }),
790
824
  /* @__PURE__ */ jsx(InputRow, { label: shoeField.label, fieldKey: shoeField.key, placeholder: shoeField.ph })
791
825
  ] })
792
- ] }),
826
+ ] }) : /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "ps-tryon-input-row", children: [
827
+ /* @__PURE__ */ jsx("label", { children: "Weight" }),
828
+ /* @__PURE__ */ jsx("input", { type: "number", placeholder: weightUnit === "lbs" ? "e.g. 170" : "e.g. 75", defaultValue: formRef.current.weight || "", onInput: (e) => updateField("weight", e.target.value) }),
829
+ /* @__PURE__ */ jsx(UnitToggle, { options: [{ label: "kg", value: "kg" }, { label: "lbs", value: "lbs" }], value: weightUnit, onChange: setWeightUnit })
830
+ ] }) }),
793
831
  /* @__PURE__ */ jsx("p", { className: "ps-tryon-disclaimer", children: "Fill in what you know — more measurements = better accuracy." }),
794
832
  /* @__PURE__ */ jsxs("button", { className: "ps-tryon-submit", onClick: handleSubmit, children: [
795
833
  "Get My Size & Try On ",
796
834
  /* @__PURE__ */ jsx(ArrowRightIcon, {})
797
835
  ] })
798
- ] });
836
+ ] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}`);
799
837
  }
800
838
  function ProcessingView() {
801
839
  return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
802
- countdown > 0 ? /* @__PURE__ */ jsxs("div", { className: "ps-tryon-countdown-ring", children: [
840
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-countdown-ring", children: [
803
841
  /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 120 120", children: [
804
842
  /* @__PURE__ */ jsx("circle", { className: "ps-tryon-countdown-track", cx: "60", cy: "60", r: "52" }),
805
- /* @__PURE__ */ jsx("circle", { className: "ps-tryon-countdown-progress", cx: "60", cy: "60", r: "52", style: { strokeDashoffset: `${countdown / 25 * 326.73}` } })
843
+ /* @__PURE__ */ jsx("circle", { ref: countdownCircleRef, className: "ps-tryon-countdown-progress", cx: "60", cy: "60", r: "52", style: { strokeDashoffset: `${25 / 25 * 326.73}` } })
806
844
  ] }),
807
- /* @__PURE__ */ jsx("span", { className: "ps-tryon-countdown-number", children: countdown })
808
- ] }) : /* @__PURE__ */ jsx("div", { className: "ps-tryon-done-pulse", children: /* @__PURE__ */ jsx("div", { className: cx("ps-tryon-spinner", cn.spinner) }) }),
809
- /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-text", cn.processingText), children: countdown > 0 ? "Generating your try-on..." : "Almost there..." }),
810
- /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-sub", cn.processingSubText), children: countdown > 0 ? "This usually takes 15-25 seconds" : "Finishing up your look" })
845
+ /* @__PURE__ */ jsx("span", { ref: countdownElRef, className: "ps-tryon-countdown-number", children: "25" })
846
+ ] }),
847
+ /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-text", cn.processingText), children: "Generating your try-on..." }),
848
+ /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-sub", cn.processingSubText), children: "This usually takes 15-25 seconds" })
811
849
  ] });
812
850
  }
813
851
  function ResultView() {
@@ -851,14 +889,17 @@ function PrimeStyleTryon({
851
889
  /* @__PURE__ */ jsx("button", { onClick: handleRetry, className: cx("ps-tryon-btn-retry", cn.retryButton), children: "Try Another" })
852
890
  ] }),
853
891
  sizingMethod && !profileSaved && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-prompt", children: [
854
- /* @__PURE__ */ jsx("input", { type: "text", placeholder: "Profile name (e.g. Me)", value: profileName, onChange: (e) => setProfileName(e.target.value) }),
855
- /* @__PURE__ */ jsx("button", { onClick: () => {
856
- if (profileName.trim()) saveProfile(profileName.trim());
857
- }, children: "Save" })
892
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-save-label", children: "Save your measurements for next time" }),
893
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-row", children: [
894
+ /* @__PURE__ */ jsx("input", { type: "text", placeholder: "Name this profile (e.g. John, Sarah)", value: profileName, onChange: (e) => setProfileName(e.target.value) }),
895
+ /* @__PURE__ */ jsx("button", { onClick: () => {
896
+ if (profileName.trim()) saveProfile(profileName.trim());
897
+ }, children: "Save" })
898
+ ] })
858
899
  ] }),
859
900
  profileSaved && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-done", children: [
860
901
  /* @__PURE__ */ jsx(CheckIcon, {}),
861
- " Profile saved"
902
+ " Measurements saved to profile"
862
903
  ] })
863
904
  ] })
864
905
  ] });
@@ -966,19 +1007,19 @@ function PrimeStyleTryon({
966
1007
  function renderBody() {
967
1008
  switch (view) {
968
1009
  case "welcome":
969
- return /* @__PURE__ */ jsx(WelcomeView, {});
1010
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(WelcomeView, {}) }, "v-welcome");
970
1011
  case "upload":
971
- return /* @__PURE__ */ jsx(UploadView, {});
1012
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(UploadView, {}) }, "v-upload");
972
1013
  case "sizing-choice":
973
- return /* @__PURE__ */ jsx(SizingChoiceView, {});
1014
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(SizingChoiceView, {}) }, "v-choice");
974
1015
  case "sizing-form":
975
- return /* @__PURE__ */ jsx(SizingFormView, {});
1016
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(SizingFormView, {}) }, "v-form");
976
1017
  case "processing":
977
- return /* @__PURE__ */ jsx(ProcessingView, {});
1018
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(ProcessingView, {}) }, "v-proc");
978
1019
  case "result":
979
- return /* @__PURE__ */ jsx(ResultView, {});
1020
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(ResultView, {}) }, "v-result");
980
1021
  case "error":
981
- return /* @__PURE__ */ jsx(ErrorView, {});
1022
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(ErrorView, {}) }, "v-error");
982
1023
  default:
983
1024
  return null;
984
1025
  }
@@ -1095,8 +1136,8 @@ const STYLES = `
1095
1136
 
1096
1137
  /* Body */
1097
1138
  .ps-tryon-body { padding: 24px; min-height: 300px; }
1098
- .ps-tryon-body > * { animation: ps-fade-up 0.35s ease both; }
1099
1139
  @keyframes ps-fade-up { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
1140
+ .ps-tryon-view-enter { animation: ps-fade-up 0.35s ease both; }
1100
1141
 
1101
1142
  /* Welcome */
1102
1143
  .ps-tryon-welcome { text-align: center; }
@@ -1168,28 +1209,25 @@ const STYLES = `
1168
1209
 
1169
1210
  /* Sizing choice */
1170
1211
  .ps-tryon-sizing-choice { text-align: center; }
1171
- .ps-tryon-section-title { font-size: 16px; font-weight: 600; color: #fff; margin: 0 0 20px; }
1172
- .ps-tryon-choice-cards { display: flex; flex-direction: column; gap: 12px; }
1212
+ .ps-tryon-section-title { font-size: 16px; font-weight: 600; color: #fff; margin: 0 0 16px; }
1213
+ .ps-tryon-choice-cards { display: flex; flex-direction: column; gap: 10px; }
1173
1214
  .ps-tryon-choice-card {
1174
- padding: 20px; border: 1.5px solid #333; border-radius: 14px;
1215
+ display: flex; align-items: center; gap: 14px; padding: 16px;
1216
+ border: 1.5px solid #333; border-radius: 12px;
1175
1217
  background: #1a1b1a; cursor: pointer; transition: all 0.25s; text-align: left;
1176
- display: block; width: 100%; font-family: inherit;
1218
+ width: 100%; font-family: inherit; position: relative; overflow: hidden;
1177
1219
  }
1178
- .ps-tryon-choice-card:hover { border-color: #bb945c; box-shadow: 0 4px 20px rgba(187,148,92,0.08); }
1179
- .ps-tryon-choice-icon { color: #bb945c; margin-bottom: 8px; }
1220
+ .ps-tryon-choice-card:hover { border-color: #bb945c; transform: translateY(-1px); box-shadow: 0 6px 20px rgba(187,148,92,0.08); }
1221
+ .ps-tryon-choice-card:hover::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: #bb945c; }
1222
+ .ps-tryon-choice-icon { color: #bb945c; flex-shrink: 0; }
1180
1223
  .ps-tryon-choice-icon svg { stroke: currentColor; fill: none; }
1181
- .ps-tryon-choice-title { font-size: 15px; font-weight: 600; color: #fff; margin-bottom: 4px; }
1182
- .ps-tryon-choice-desc { font-size: 12px; color: #999; }
1224
+ .ps-tryon-choice-info { flex: 1; min-width: 0; }
1225
+ .ps-tryon-choice-title { font-size: 14px; font-weight: 600; color: #fff; }
1226
+ .ps-tryon-choice-desc { font-size: 12px; color: #999; margin-top: 2px; }
1183
1227
  .ps-tryon-choice-badge {
1184
- display: inline-block; margin-top: 8px; padding: 3px 10px; border-radius: 20px;
1185
- background: rgba(187,148,92,0.12); color: #bb945c; font-size: 11px; font-weight: 600;
1186
- }
1187
- .ps-tryon-skip-btn {
1188
- margin-top: 16px; padding: 10px; background: none; border: 1px solid #333;
1189
- border-radius: 10px; color: #999; font-size: 13px; cursor: pointer; transition: all 0.2s;
1190
- width: 100%; font-family: inherit;
1228
+ padding: 3px 10px; border-radius: 20px; flex-shrink: 0;
1229
+ background: rgba(187,148,92,0.12); color: #bb945c; font-size: 10px; font-weight: 600;
1191
1230
  }
1192
- .ps-tryon-skip-btn:hover { border-color: #666; color: #ccc; }
1193
1231
 
1194
1232
  /* Sizing form */
1195
1233
  .ps-tryon-sizing-form { display: flex; flex-direction: column; gap: 12px; }
@@ -1204,8 +1242,11 @@ const STYLES = `
1204
1242
  .ps-tryon-input-row input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
1205
1243
  .ps-tryon-input-row input:focus { border-color: #bb945c; }
1206
1244
  .ps-tryon-input-unit { font-size: 12px; color: #666; flex-shrink: 0; }
1207
- .ps-tryon-dual-input { gap: 8px; }
1208
- .ps-tryon-dual-input input { width: 50%; }
1245
+ .ps-tryon-height-ft { display: flex; align-items: center; gap: 6px; flex: 1; }
1246
+ .ps-tryon-height-ft input { width: 60px; padding: 10px 10px; border: 1.5px solid #333; border-radius: 10px; background: #1a1b1a; color: #fff; font-size: 14px; font-family: inherit; outline: none; text-align: center; -moz-appearance: textfield; }
1247
+ .ps-tryon-height-ft input::-webkit-outer-spin-button, .ps-tryon-height-ft input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
1248
+ .ps-tryon-height-ft input:focus { border-color: #bb945c; }
1249
+ .ps-tryon-height-ft span { font-size: 12px; color: #666; }
1209
1250
  .ps-tryon-country-select {
1210
1251
  flex: 1; padding: 10px 36px 10px 14px; border: 1.5px solid #333; border-radius: 10px;
1211
1252
  background: #1a1b1a; color: #fff; font-size: 13px; font-family: inherit;
@@ -1280,17 +1321,19 @@ const STYLES = `
1280
1321
  .ps-fit-good { color: #4ade80; } .ps-fit-tight { color: #f59e0b; } .ps-fit-loose { color: #60a5fa; }
1281
1322
 
1282
1323
  /* Save profile prompt */
1283
- .ps-tryon-save-prompt { display: flex; gap: 8px; margin-top: 14px; }
1284
- .ps-tryon-save-prompt input {
1324
+ .ps-tryon-save-prompt { margin-top: 14px; padding: 14px; border: 1px solid #333; border-radius: 12px; background: #1a1b1a; }
1325
+ .ps-tryon-save-label { font-size: 12px; color: #999; margin-bottom: 10px; }
1326
+ .ps-tryon-save-row { display: flex; gap: 8px; }
1327
+ .ps-tryon-save-row input {
1285
1328
  flex: 1; padding: 10px 14px; border: 1.5px solid #333; border-radius: 10px;
1286
- background: #1a1b1a; color: #fff; font-size: 13px; font-family: inherit; outline: none;
1329
+ background: #111211; color: #fff; font-size: 13px; font-family: inherit; outline: none;
1287
1330
  }
1288
- .ps-tryon-save-prompt input:focus { border-color: #bb945c; }
1289
- .ps-tryon-save-prompt button {
1331
+ .ps-tryon-save-row input:focus { border-color: #bb945c; }
1332
+ .ps-tryon-save-row button {
1290
1333
  padding: 10px 20px; background: #bb945c; color: #111; border: none; border-radius: 10px;
1291
1334
  font-size: 13px; font-weight: 600; cursor: pointer; transition: opacity 0.2s; font-family: inherit;
1292
1335
  }
1293
- .ps-tryon-save-prompt button:hover { opacity: 0.9; }
1336
+ .ps-tryon-save-row button:hover { opacity: 0.9; }
1294
1337
  .ps-tryon-save-done { font-size: 12px; color: #4ade80; margin-top: 10px; display: flex; align-items: center; gap: 6px; justify-content: center; }
1295
1338
  .ps-tryon-save-done svg { stroke: currentColor; }
1296
1339
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",