@primestyleai/tryon 3.6.6 → 3.8.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.
@@ -27,7 +27,7 @@ export interface PrimeStyleTryonProps {
27
27
  message: string;
28
28
  code?: string;
29
29
  }) => void;
30
- /** Size guide data from your backend — pass as-is, any format (HTML, JSON, object, string). Our AI will parse it. */
30
+ /** Size guide data — pass as-is, any format (JSON object, array, HTML). Structured formats are parsed instantly client-side; unrecognised formats fall back to AI. */
31
31
  sizeGuideData?: unknown;
32
32
  }
33
33
  export declare function PrimeStyleTryon(props: PrimeStyleTryonProps): import("react/jsx-runtime").JSX.Element | null;
@@ -2,6 +2,365 @@
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import { useState, useEffect, useRef, useMemo, useCallback } from "react";
4
4
  import { A as ApiClient, S as SseClient, i as isValidImageFile, c as compressImage, P as PrimeStyleError } from "../image-utils-C9bJ1zKO.js";
5
+ const HEADER_ALIASES = {
6
+ // ── Size label columns (skipped during field derivation) ──
7
+ size: "__size__",
8
+ sizes: "__size__",
9
+ label: "__size__",
10
+ taglia: "__size__",
11
+ // IT
12
+ taille: "__size__",
13
+ // FR
14
+ größe: "__size__",
15
+ // DE
16
+ groesse: "__size__",
17
+ tamaño: "__size__",
18
+ // ES
19
+ サイズ: "__size__",
20
+ // JP
21
+ // ── Chest / Bust ──
22
+ chest: "chest",
23
+ "chest (cm)": "chest",
24
+ "chest(cm)": "chest",
25
+ "chest (in)": "chest",
26
+ bust: "bust",
27
+ "bust (cm)": "bust",
28
+ "bust(cm)": "bust",
29
+ "bust (in)": "bust",
30
+ "chest/bust": "chest",
31
+ "chest / bust": "chest",
32
+ petto: "chest",
33
+ // IT
34
+ poitrine: "bust",
35
+ // FR
36
+ brust: "chest",
37
+ // DE
38
+ // ── Waist ──
39
+ waist: "waist",
40
+ "waist (cm)": "waist",
41
+ "waist(cm)": "waist",
42
+ "waist (in)": "waist",
43
+ vita: "waist",
44
+ // IT
45
+ taille_mesure: "waist",
46
+ // FR (disambiguation from size label)
47
+ "tour de taille": "waist",
48
+ taille_cm: "waist",
49
+ // ── Hips ──
50
+ hips: "hips",
51
+ hip: "hips",
52
+ "hips (cm)": "hips",
53
+ "hip (cm)": "hips",
54
+ "hips(cm)": "hips",
55
+ "hips (in)": "hips",
56
+ fianchi: "hips",
57
+ // IT
58
+ hanches: "hips",
59
+ // FR
60
+ hüfte: "hips",
61
+ // DE
62
+ // ── Shoulder ──
63
+ shoulder: "shoulderWidth",
64
+ shoulders: "shoulderWidth",
65
+ "shoulder width": "shoulderWidth",
66
+ "shoulder (cm)": "shoulderWidth",
67
+ "shoulders (cm)": "shoulderWidth",
68
+ "shoulder width (cm)": "shoulderWidth",
69
+ spalle: "shoulderWidth",
70
+ // IT
71
+ épaules: "shoulderWidth",
72
+ // FR
73
+ schulter: "shoulderWidth",
74
+ // DE
75
+ // ── Sleeve ──
76
+ sleeve: "sleeveLength",
77
+ "sleeve length": "sleeveLength",
78
+ "sleeve (cm)": "sleeveLength",
79
+ "sleeve length (cm)": "sleeveLength",
80
+ manica: "sleeveLength",
81
+ // IT
82
+ manche: "sleeveLength",
83
+ // FR
84
+ ärmel: "sleeveLength",
85
+ // DE
86
+ // ── Inseam ──
87
+ inseam: "inseam",
88
+ "inseam (cm)": "inseam",
89
+ "inseam(cm)": "inseam",
90
+ "inseam (in)": "inseam",
91
+ "inside leg": "inseam",
92
+ "inside leg (cm)": "inseam",
93
+ cavallo: "inseam",
94
+ // IT
95
+ entrejambe: "inseam",
96
+ // FR
97
+ // ── Neck ──
98
+ neck: "neckCircumference",
99
+ collar: "neckCircumference",
100
+ "neck (cm)": "neckCircumference",
101
+ "collar (cm)": "neckCircumference",
102
+ collo: "neckCircumference",
103
+ // IT
104
+ col: "neckCircumference",
105
+ // FR
106
+ // ── Length (body/garment) ──
107
+ length: "length",
108
+ "body length": "length",
109
+ "length (cm)": "length",
110
+ "body length (cm)": "length",
111
+ lunghezza: "length",
112
+ // IT
113
+ longueur: "length",
114
+ // FR
115
+ // ── Thigh ──
116
+ thigh: "thighCircumference",
117
+ "thigh (cm)": "thighCircumference",
118
+ coscia: "thighCircumference",
119
+ // IT
120
+ // ── Foot / Shoe ──
121
+ "foot length": "footLengthCm",
122
+ "foot length (cm)": "footLengthCm",
123
+ "foot (cm)": "footLengthCm",
124
+ cm: "footLengthCm",
125
+ eu: "shoeEU",
126
+ "eu size": "shoeEU",
127
+ eur: "shoeEU",
128
+ us: "shoeUS",
129
+ "us size": "shoeUS",
130
+ uk: "shoeUK",
131
+ "uk size": "shoeUK"
132
+ };
133
+ const FIELD_LABELS = {
134
+ chest: "Chest",
135
+ bust: "Bust",
136
+ waist: "Waist",
137
+ hips: "Hips",
138
+ shoulderWidth: "Shoulders",
139
+ sleeveLength: "Sleeve Length",
140
+ inseam: "Inseam",
141
+ neckCircumference: "Neck",
142
+ length: "Length",
143
+ thighCircumference: "Thigh",
144
+ footLengthCm: "Foot Length",
145
+ shoeEU: "Shoe Size (EU)",
146
+ shoeUS: "Shoe Size (US)",
147
+ shoeUK: "Shoe Size (UK)"
148
+ };
149
+ const FIELD_PLACEHOLDERS = {
150
+ chest: "e.g. 104",
151
+ bust: "e.g. 88",
152
+ waist: "e.g. 84",
153
+ hips: "e.g. 96",
154
+ shoulderWidth: "e.g. 46",
155
+ sleeveLength: "e.g. 64",
156
+ inseam: "e.g. 81",
157
+ neckCircumference: "e.g. 40",
158
+ length: "e.g. 72",
159
+ thighCircumference: "e.g. 58",
160
+ footLengthCm: "e.g. 27",
161
+ shoeEU: "e.g. 42",
162
+ shoeUS: "e.g. 9",
163
+ shoeUK: "e.g. 8"
164
+ };
165
+ const SIZE_LABEL_PATTERN = /^(XXS|XS|S|M|L|XL|XXL|XXXL|2XL|3XL|4XL|5XL|\d{2,3}(\/\d{2,3})?(R|S|L)?|ONE\s*SIZE)$/i;
166
+ const SHOE_FIELD_KEYS = /* @__PURE__ */ new Set(["shoeEU", "shoeUS", "shoeUK", "footLengthCm"]);
167
+ function resolveHeaderKey(header) {
168
+ const cleaned = header.toLowerCase().trim().replace(/\(.*?\)/g, "").trim();
169
+ return HEADER_ALIASES[cleaned] ?? HEADER_ALIASES[header.toLowerCase().trim()] ?? null;
170
+ }
171
+ function deriveRequiredFields(headers) {
172
+ const fields = [];
173
+ const seen = /* @__PURE__ */ new Set();
174
+ for (const h of headers) {
175
+ const fieldKey = resolveHeaderKey(h);
176
+ if (!fieldKey || fieldKey === "__size__" || seen.has(fieldKey)) continue;
177
+ seen.add(fieldKey);
178
+ const isShoe = SHOE_FIELD_KEYS.has(fieldKey);
179
+ fields.push({
180
+ key: fieldKey,
181
+ label: FIELD_LABELS[fieldKey] || h,
182
+ required: true,
183
+ unit: isShoe && fieldKey !== "footLengthCm" ? "size" : "cm",
184
+ placeholder: FIELD_PLACEHOLDERS[fieldKey] || "",
185
+ category: isShoe ? "shoe" : "body"
186
+ });
187
+ }
188
+ fields.sort((a, b) => {
189
+ if (a.category !== b.category) return a.category === "body" ? -1 : 1;
190
+ return 0;
191
+ });
192
+ return fields;
193
+ }
194
+ function isObject(v) {
195
+ return typeof v === "object" && v !== null && !Array.isArray(v);
196
+ }
197
+ function isSizeLabel(key) {
198
+ return SIZE_LABEL_PATTERN.test(key.trim());
199
+ }
200
+ function allKeysAreSizeLabels(obj) {
201
+ const keys = Object.keys(obj);
202
+ if (keys.length === 0) return false;
203
+ return keys.every((k) => isSizeLabel(k));
204
+ }
205
+ function allKeysAreSectionNames(obj) {
206
+ const keys = Object.keys(obj);
207
+ if (keys.length < 2) return false;
208
+ return keys.every((k) => {
209
+ const val = obj[k];
210
+ const isContainer = Array.isArray(val) || isObject(val);
211
+ const isNotSizeLabel = !isSizeLabel(k);
212
+ const isNotMeasurement = !HEADER_ALIASES[k.toLowerCase().trim()];
213
+ return isContainer && isNotSizeLabel && isNotMeasurement;
214
+ });
215
+ }
216
+ function tryPreNormalized(input) {
217
+ if (!isObject(input)) return null;
218
+ const obj = input;
219
+ if (obj.found === true && Array.isArray(obj.headers) && Array.isArray(obj.rows) && obj.headers.length > 0 && obj.rows.length > 0) {
220
+ const headers = obj.headers;
221
+ const rows = obj.rows.map((r) => r.map(String));
222
+ return {
223
+ found: true,
224
+ title: typeof obj.title === "string" ? obj.title : void 0,
225
+ headers,
226
+ rows,
227
+ requiredFields: Array.isArray(obj.requiredFields) && obj.requiredFields.length > 0 ? obj.requiredFields : deriveRequiredFields(headers)
228
+ };
229
+ }
230
+ return null;
231
+ }
232
+ function tryKeyValueObject(input) {
233
+ if (!isObject(input)) return null;
234
+ const obj = input;
235
+ if (!allKeysAreSizeLabels(obj)) return null;
236
+ const sizeLabels = Object.keys(obj);
237
+ const firstVal = obj[sizeLabels[0]];
238
+ if (!isObject(firstVal)) return null;
239
+ const measureKeySet = /* @__PURE__ */ new Set();
240
+ for (const label of sizeLabels) {
241
+ const sizeObj = obj[label];
242
+ if (!isObject(sizeObj)) return null;
243
+ for (const k of Object.keys(sizeObj)) {
244
+ measureKeySet.add(k);
245
+ }
246
+ }
247
+ const measureKeys = Array.from(measureKeySet);
248
+ const headers = ["Size", ...measureKeys];
249
+ const rows = sizeLabels.map((label) => {
250
+ const sizeObj = obj[label];
251
+ return [label, ...measureKeys.map((k) => String(sizeObj[k] ?? ""))];
252
+ });
253
+ return { found: true, headers, rows, requiredFields: deriveRequiredFields(headers) };
254
+ }
255
+ function tryArrayOfObjects(input) {
256
+ if (!Array.isArray(input) || input.length === 0) return null;
257
+ if (!isObject(input[0])) return null;
258
+ const first = input[0];
259
+ const sizeKey = Object.keys(first).find((k) => {
260
+ const alias = HEADER_ALIASES[k.toLowerCase().trim()];
261
+ return alias === "__size__";
262
+ }) || Object.keys(first).find((k) => k.toLowerCase() === "size" || k.toLowerCase() === "label");
263
+ if (!sizeKey) return null;
264
+ const allKeys = /* @__PURE__ */ new Set();
265
+ for (const row of input) {
266
+ if (!isObject(row)) return null;
267
+ for (const k of Object.keys(row)) {
268
+ allKeys.add(k);
269
+ }
270
+ }
271
+ const measureKeys = Array.from(allKeys).filter((k) => k !== sizeKey);
272
+ const headers = [sizeKey, ...measureKeys];
273
+ const rows = input.map((row) => {
274
+ const obj = row;
275
+ return headers.map((h) => String(obj[h] ?? ""));
276
+ });
277
+ const displayHeaders = headers.map((h) => h.charAt(0).toUpperCase() + h.slice(1));
278
+ return { found: true, headers: displayHeaders, rows, requiredFields: deriveRequiredFields(displayHeaders) };
279
+ }
280
+ function tryArrayOfArrays(input) {
281
+ if (!Array.isArray(input) || input.length < 2) return null;
282
+ if (!Array.isArray(input[0])) return null;
283
+ const headers = input[0].map(String);
284
+ const rows = input.slice(1).map((r) => r.map(String));
285
+ if (headers.length < 2) return null;
286
+ return { found: true, headers, rows, requiredFields: deriveRequiredFields(headers) };
287
+ }
288
+ function tryWrapperObject(input) {
289
+ if (!isObject(input)) return null;
290
+ const obj = input;
291
+ const arrayKey = ["sizes", "data", "sizeChart", "size_chart", "sizeGuide", "size_guide", "chart"].find(
292
+ (k) => Array.isArray(obj[k])
293
+ );
294
+ if (!arrayKey) return null;
295
+ const inner = obj[arrayKey];
296
+ return tryArrayOfObjects(inner) ?? tryArrayOfArrays(inner);
297
+ }
298
+ function tryMultiSection(input) {
299
+ if (!isObject(input)) return null;
300
+ const obj = input;
301
+ if (!allKeysAreSectionNames(obj)) return null;
302
+ const sections = {};
303
+ let primaryHeaders = [];
304
+ let primaryRows = [];
305
+ const allFieldKeys = /* @__PURE__ */ new Set();
306
+ const allFields = [];
307
+ for (const [sectionName, sectionData] of Object.entries(obj)) {
308
+ const normalized = tryArrayOfObjects(sectionData) ?? tryArrayOfArrays(sectionData) ?? tryKeyValueObject(sectionData) ?? tryWrapperObject(sectionData);
309
+ if (!normalized) return null;
310
+ sections[sectionName] = {
311
+ headers: normalized.headers,
312
+ rows: normalized.rows,
313
+ requiredFields: normalized.requiredFields
314
+ };
315
+ if (primaryHeaders.length === 0) {
316
+ primaryHeaders = normalized.headers;
317
+ primaryRows = normalized.rows;
318
+ }
319
+ for (const f of normalized.requiredFields) {
320
+ if (!allFieldKeys.has(f.key)) {
321
+ allFieldKeys.add(f.key);
322
+ allFields.push(f);
323
+ }
324
+ }
325
+ }
326
+ if (Object.keys(sections).length === 0) return null;
327
+ return {
328
+ found: true,
329
+ headers: primaryHeaders,
330
+ rows: primaryRows,
331
+ requiredFields: allFields,
332
+ sections
333
+ };
334
+ }
335
+ function tryJsonString(input) {
336
+ if (typeof input !== "string") return null;
337
+ const trimmed = input.trim();
338
+ if (trimmed.startsWith("<") || trimmed.includes("<table") || trimmed.includes("<tr")) {
339
+ return null;
340
+ }
341
+ try {
342
+ const parsed = JSON.parse(trimmed);
343
+ return normalizeSizeGuide(parsed).success ? normalizeSizeGuide(parsed).data : null;
344
+ } catch {
345
+ return null;
346
+ }
347
+ }
348
+ function normalizeSizeGuide(input) {
349
+ if (input === null || input === void 0) {
350
+ return { success: false, reason: "no data provided" };
351
+ }
352
+ const result = tryPreNormalized(input) ?? tryMultiSection(input) ?? tryKeyValueObject(input) ?? tryArrayOfObjects(input) ?? tryArrayOfArrays(input) ?? tryWrapperObject(input) ?? tryJsonString(input);
353
+ if (result && result.requiredFields.length > 0) {
354
+ return { success: true, data: result };
355
+ }
356
+ if (result && result.requiredFields.length === 0 && result.rows.length > 0) {
357
+ return { success: true, data: result };
358
+ }
359
+ return {
360
+ success: false,
361
+ reason: typeof input === "string" ? "unstructured string" : "unrecognised format"
362
+ };
363
+ }
5
364
  function cx(base, override) {
6
365
  return override ? `${base} ${override}` : base;
7
366
  }
@@ -51,8 +410,8 @@ const SIZING_COUNTRIES = [
51
410
  { code: "AU", label: "Australia" },
52
411
  { code: "BR", label: "Brazil" }
53
412
  ];
54
- const STEP_LABELS = ["", "Welcome", "Photo", "Size", "Generate", "Results"];
55
- const TOTAL_STEPS = 5;
413
+ const STEP_LABELS = ["", "Welcome", "Size", "Your Fit", "Try On"];
414
+ const TOTAL_STEPS = 4;
56
415
  function detectLocale() {
57
416
  if (typeof window === "undefined") return "US";
58
417
  const l = (navigator.language || "en-US").toLowerCase();
@@ -78,22 +437,6 @@ function lbsToKg(lbs) {
78
437
  function ftInToCm(ft, inch) {
79
438
  return +(ft * 30.48 + inch * 2.54).toFixed(1);
80
439
  }
81
- function SvgIcon({ d, size = 18, strokeWidth = 2 }) {
82
- return /* @__PURE__ */ jsx(
83
- "svg",
84
- {
85
- width: size,
86
- height: size,
87
- viewBox: "0 0 24 24",
88
- fill: "none",
89
- stroke: "currentColor",
90
- strokeWidth,
91
- strokeLinecap: "round",
92
- strokeLinejoin: "round",
93
- children: /* @__PURE__ */ jsx("path", { d })
94
- }
95
- );
96
- }
97
440
  function CameraIcon({ size = 18 }) {
98
441
  return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
99
442
  /* @__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" }),
@@ -327,13 +670,25 @@ function PrimeStyleTryonInner({
327
670
  useEffect(() => {
328
671
  lsSet("history", history);
329
672
  }, [history]);
673
+ const normalizedGuide = useMemo(() => {
674
+ if (!sizeGuideData) return null;
675
+ return normalizeSizeGuide(sizeGuideData);
676
+ }, [sizeGuideData]);
330
677
  useEffect(() => {
331
- if (view !== "sizing-choice" || sizeGuideFetchedRef.current || !apiRef.current) return;
678
+ if (view !== "sizing-choice" || sizeGuideFetchedRef.current) return;
332
679
  sizeGuideFetchedRef.current = true;
333
680
  if (!sizeGuideData) {
334
681
  setSizeGuide({ found: false });
335
682
  return;
336
683
  }
684
+ if (normalizedGuide?.success) {
685
+ setSizeGuide(normalizedGuide.data);
686
+ return;
687
+ }
688
+ if (!apiRef.current) {
689
+ setSizeGuide({ found: false });
690
+ return;
691
+ }
337
692
  setSizeGuideFetching(true);
338
693
  const baseUrl = getApiUrl(apiUrl);
339
694
  const key = getApiKey();
@@ -345,20 +700,20 @@ function PrimeStyleTryonInner({
345
700
  if (data) setSizeGuide(data);
346
701
  else setSizeGuide({ found: false });
347
702
  }).catch(() => setSizeGuide({ found: false })).finally(() => setSizeGuideFetching(false));
348
- }, [view, apiUrl, productTitle, sizeGuideData]);
703
+ }, [view, apiUrl, productTitle, sizeGuideData, normalizedGuide]);
349
704
  const stepIndex = useMemo(() => {
350
705
  switch (view) {
351
706
  case "welcome":
352
707
  return 1;
353
- case "upload":
354
- return 2;
355
708
  case "sizing-choice":
356
709
  case "sizing-form":
710
+ return 2;
711
+ case "size-result":
357
712
  return 3;
713
+ case "upload":
358
714
  case "processing":
359
- return 4;
360
715
  case "result":
361
- return 5;
716
+ return 4;
362
717
  default:
363
718
  return 1;
364
719
  }
@@ -385,6 +740,7 @@ function PrimeStyleTryonInner({
385
740
  setFormGender("male");
386
741
  sizeGuideFetchedRef.current = false;
387
742
  setSizeGuideFetching(false);
743
+ historySavedRef.current = false;
388
744
  unsubRef.current?.();
389
745
  unsubRef.current = null;
390
746
  if (pollingRef.current) {
@@ -466,7 +822,10 @@ function PrimeStyleTryonInner({
466
822
  locale: sizingCountry,
467
823
  product: { title: productTitle, description: "", variants: [] }
468
824
  };
469
- if (sizeGuide?.found) payload.sizeGuide = sizeGuide;
825
+ if (sizeGuide?.found) {
826
+ payload.sizeGuide = sizeGuide;
827
+ if (sizeGuide.sections) payload.sizeGuide = { ...sizeGuide, sections: sizeGuide.sections };
828
+ }
470
829
  payload.sizingUnit = sizingUnit;
471
830
  if (sizingMethod === "exact") {
472
831
  const m = { gender: formRef.current.gender || "male", sizingUnit };
@@ -514,7 +873,7 @@ function PrimeStyleTryonInner({
514
873
  setSizingLoading(false);
515
874
  }
516
875
  }, [apiUrl, sizingMethod, sizingCountry, heightUnit, weightUnit, sizingUnit, sizeGuide, productTitle, dynamicFields]);
517
- const handleSubmit = useCallback(async () => {
876
+ const handleTryOnSubmit = useCallback(async () => {
518
877
  if (!selectedFile || !apiRef.current || !sseRef.current) {
519
878
  const msg = !apiRef.current ? "Missing NEXT_PUBLIC_PRIMESTYLE_API_KEY" : "No file selected";
520
879
  setErrorMessage(msg);
@@ -524,10 +883,6 @@ function PrimeStyleTryonInner({
524
883
  }
525
884
  completedRef.current = false;
526
885
  setView("processing");
527
- if (sizingMethod) {
528
- setSizingLoading(true);
529
- submitSizing();
530
- }
531
886
  try {
532
887
  const modelImage = await compressImage(selectedFile);
533
888
  const response = await apiRef.current.submitTryOn(modelImage, productImage);
@@ -561,7 +916,7 @@ function PrimeStyleTryonInner({
561
916
  setView("error");
562
917
  onError?.({ message, code });
563
918
  }
564
- }, [selectedFile, productImage, onProcessing, onError, handleVtoUpdate, sizingMethod, submitSizing]);
919
+ }, [selectedFile, productImage, onProcessing, onError, handleVtoUpdate]);
565
920
  const handleDownload = useCallback(() => {
566
921
  if (!resultImageUrl) return;
567
922
  if (resultImageUrl.startsWith("data:")) {
@@ -592,7 +947,7 @@ function PrimeStyleTryonInner({
592
947
  setSizingResult(null);
593
948
  setSizingLoading(false);
594
949
  setProfileSaved(false);
595
- setView("upload");
950
+ setView("sizing-choice");
596
951
  }, [previewUrl, cleanupJob]);
597
952
  const applyProfile = useCallback((id) => {
598
953
  const p = profiles.find((pr) => pr.id === id);
@@ -712,11 +1067,18 @@ function PrimeStyleTryonInner({
712
1067
  }
713
1068
  setHistory((prev) => [entry, ...prev].slice(0, 50));
714
1069
  }, [productTitle, productImage, resultImageUrl, sizingResult, activeProfileId, profiles]);
1070
+ const historySavedRef = useRef(false);
715
1071
  useEffect(() => {
716
- if (view === "result" && (resultImageUrl || sizingResult)) {
1072
+ if (view === "size-result" && sizingResult && !historySavedRef.current) {
1073
+ historySavedRef.current = true;
1074
+ saveHistoryEntry();
1075
+ } else if (view === "result" && resultImageUrl && !historySavedRef.current) {
1076
+ historySavedRef.current = true;
717
1077
  saveHistoryEntry();
1078
+ } else if (view === "welcome" || view === "sizing-choice") {
1079
+ historySavedRef.current = false;
718
1080
  }
719
- }, [view]);
1081
+ }, [view, sizingResult, resultImageUrl]);
720
1082
  const updateField = useCallback((key, val) => {
721
1083
  formRef.current[key] = val;
722
1084
  }, []);
@@ -800,20 +1162,19 @@ function PrimeStyleTryonInner({
800
1162
  /* @__PURE__ */ jsx("img", { src: productImage, alt: "Product", className: "ps-tryon-welcome-product" }),
801
1163
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-welcome-sparkle", children: /* @__PURE__ */ jsx(SparkleIcon, { size: 20 }) })
802
1164
  ] }),
803
- /* @__PURE__ */ jsx("h2", { className: "ps-tryon-welcome-title", children: "See How It Looks On You" }),
804
- /* @__PURE__ */ jsx("p", { className: "ps-tryon-welcome-sub", children: "Virtual try-on + AI size recommendation" })
1165
+ /* @__PURE__ */ jsx("h2", { className: "ps-tryon-welcome-title", children: "Find Your Perfect Size" }),
1166
+ /* @__PURE__ */ jsx("p", { className: "ps-tryon-welcome-sub", children: "Get your size instantly, then try it on" })
805
1167
  ] }),
806
1168
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-features", children: [
807
- { icon: /* @__PURE__ */ jsx(CameraIcon, {}), title: "Upload Photo", desc: "Share a photo of yourself" },
808
- { icon: /* @__PURE__ */ jsx(RulerIcon, {}), title: "Find Your Size", desc: "AI-powered fit analysis" },
809
- { icon: /* @__PURE__ */ jsx(SparkleIcon, {}), title: "See Results", desc: "Try-on in seconds" }
1169
+ { icon: /* @__PURE__ */ jsx(RulerIcon, {}), title: "Get Your Size", desc: "Instant fit recommendation" },
1170
+ { icon: /* @__PURE__ */ jsx(CameraIcon, {}), title: "Try It On", desc: "See how it looks on you" }
810
1171
  ].map((f, i) => /* @__PURE__ */ jsxs("div", { className: "ps-tryon-feature", children: [
811
1172
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-feature-icon", children: f.icon }),
812
1173
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-feature-title", children: f.title }),
813
1174
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-feature-desc", children: f.desc })
814
1175
  ] }, i)) }),
815
- /* @__PURE__ */ jsxs("button", { className: "ps-tryon-cta", onClick: () => setView("upload"), children: [
816
- "Let’s Get Started ",
1176
+ /* @__PURE__ */ jsxs("button", { className: "ps-tryon-cta", onClick: () => setView("sizing-choice"), children: [
1177
+ "Find My Size ",
817
1178
  /* @__PURE__ */ jsx(ArrowRightIcon, {})
818
1179
  ] }),
819
1180
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-welcome-note", children: "Takes less than a minute" })
@@ -826,8 +1187,8 @@ function PrimeStyleTryonInner({
826
1187
  /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "Your photo", className: cn.previewImage }),
827
1188
  /* @__PURE__ */ jsx("button", { onClick: handleRemovePreview, className: cx("ps-tryon-preview-remove", cn.removeButton), children: "×" })
828
1189
  ] }),
829
- /* @__PURE__ */ jsxs("button", { onClick: () => setView("sizing-choice"), className: cx("ps-tryon-submit", cn.submitButton), children: [
830
- "Continue to Sizing ",
1190
+ /* @__PURE__ */ jsxs("button", { onClick: handleTryOnSubmit, className: cx("ps-tryon-submit", cn.submitButton), children: [
1191
+ "Try It On ",
831
1192
  /* @__PURE__ */ jsx(ArrowRightIcon, {})
832
1193
  ] })
833
1194
  ] }) : /* @__PURE__ */ jsxs(
@@ -861,7 +1222,7 @@ function PrimeStyleTryonInner({
861
1222
  }
862
1223
  ),
863
1224
  /* @__PURE__ */ jsx(UploadIcon, {}),
864
- /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-upload-text", cn.uploadText), children: "Drop or upload your full body photo!" }),
1225
+ /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-upload-text", cn.uploadText), children: "Upload a full body photo" }),
865
1226
  /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-upload-hint", cn.uploadHint), children: "JPEG, PNG or WebP (max 10MB)" })
866
1227
  ]
867
1228
  }
@@ -909,12 +1270,12 @@ function PrimeStyleTryonInner({
909
1270
  ] }),
910
1271
  /* @__PURE__ */ jsxs("button", { className: "ps-tryon-choice-card", onClick: () => {
911
1272
  setSizingMethod(null);
912
- handleSubmit();
1273
+ setView("upload");
913
1274
  }, children: [
914
- /* @__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 }) }),
1275
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-icon", children: /* @__PURE__ */ jsx(CameraIcon, { size: 24 }) }),
915
1276
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-choice-info", children: [
916
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-title", children: "Skip sizing" }),
917
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-desc", children: "Just show me the try-on" })
1277
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-title", children: "Skip, just try it on" }),
1278
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-desc", children: "Upload a photo to see how it looks" })
918
1279
  ] })
919
1280
  ] })
920
1281
  ] })
@@ -966,9 +1327,17 @@ function PrimeStyleTryonInner({
966
1327
  sizingMethod === "exact" ? /* @__PURE__ */ jsx(Fragment, { children: (() => {
967
1328
  const reqFields = dynamicFields.filter((f) => f.required);
968
1329
  const optFields = dynamicFields.filter((f) => !f.required);
1330
+ const isShoeProduct = reqFields.some((f) => f.category === "shoe");
1331
+ const isClothingProduct = reqFields.some((f) => f.category === "body");
969
1332
  const renderField = (field) => {
970
1333
  if (["shoeEU", "shoeUS", "shoeUK"].includes(field.key)) {
971
- return /* @__PURE__ */ jsx(InputRow, { label: `${field.label} *`, fieldKey: field.key, placeholder: field.placeholder }, field.key);
1334
+ const regionField = shoeField;
1335
+ const key = regionField.key;
1336
+ const showOriginal = field.key !== key;
1337
+ return /* @__PURE__ */ jsxs("span", { children: [
1338
+ /* @__PURE__ */ jsx(InputRow, { label: `${regionField.label}${field.required ? " *" : ""}`, fieldKey: key, placeholder: regionField.ph }),
1339
+ showOriginal && /* @__PURE__ */ jsx(InputRow, { label: field.label, fieldKey: field.key, placeholder: field.placeholder })
1340
+ ] }, field.key);
972
1341
  }
973
1342
  const phNum = parseFloat(field.placeholder?.replace(/[^0-9.]/g, "") || "");
974
1343
  const phIn = !isNaN(phNum) && phNum > 0 && field.unit === "cm" ? String(Math.round(phNum / 2.54)) : "";
@@ -985,6 +1354,20 @@ function PrimeStyleTryonInner({
985
1354
  field.key
986
1355
  );
987
1356
  };
1357
+ const profileExtras = [];
1358
+ if (isShoeProduct && !dynamicFields.some((f) => f.category === "body")) {
1359
+ const bodyFields = isFemale ? FALLBACK_FIELDS_FEMALE : FALLBACK_FIELDS_MALE;
1360
+ for (const f of bodyFields) {
1361
+ if (!dynamicFields.some((d) => d.key === f.key)) {
1362
+ profileExtras.push({ ...f, required: false });
1363
+ }
1364
+ }
1365
+ }
1366
+ if (isClothingProduct && !dynamicFields.some((f) => f.category === "shoe")) {
1367
+ profileExtras.push({ key: "footLengthCm", label: "Foot length", required: false, unit: "cm", placeholder: "e.g. 27", category: "shoe" });
1368
+ profileExtras.push({ key: shoeField.key, label: shoeField.label, required: false, unit: "size", placeholder: shoeField.ph, category: "shoe" });
1369
+ }
1370
+ const allOptFields = [...optFields, ...profileExtras];
988
1371
  return /* @__PURE__ */ jsxs(Fragment, { children: [
989
1372
  reqFields.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
990
1373
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-section-label", children: "Required for this product" }),
@@ -1006,7 +1389,7 @@ function PrimeStyleTryonInner({
1006
1389
  fp
1007
1390
  )) })
1008
1391
  ] }),
1009
- optFields.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-optional-section", children: [
1392
+ allOptFields.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-optional-section", children: [
1010
1393
  /* @__PURE__ */ jsxs("button", { className: "ps-tryon-optional-toggle", onClick: (e) => {
1011
1394
  const wrap = e.target.closest(".ps-tryon-optional-section").querySelector(".ps-tryon-optional-fields");
1012
1395
  const arrow = e.target.closest(".ps-tryon-optional-toggle").querySelector(".ps-tryon-chevron");
@@ -1019,18 +1402,7 @@ function PrimeStyleTryonInner({
1019
1402
  /* @__PURE__ */ jsx("span", { children: "Optional — improve accuracy & save to profile" }),
1020
1403
  /* @__PURE__ */ jsx("span", { className: "ps-tryon-chevron", children: "▾" })
1021
1404
  ] }),
1022
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-optional-fields", style: { display: "none" }, children: [
1023
- optFields.map(renderField),
1024
- !dynamicFields.some((f) => f.category === "shoe") && /* @__PURE__ */ jsxs(Fragment, { children: [
1025
- /* @__PURE__ */ jsx(InputRow, { label: "Foot length", fieldKey: "footLengthCm", placeholder: isCm ? "e.g. 27" : "e.g. 10.5", type: "number", unit: sizingUnit }),
1026
- /* @__PURE__ */ jsx(InputRow, { label: shoeField.label, fieldKey: shoeField.key, placeholder: shoeField.ph })
1027
- ] })
1028
- ] })
1029
- ] }),
1030
- optFields.length === 0 && !dynamicFields.some((f) => f.category === "shoe") && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-shoe-section", children: [
1031
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-shoe-title", children: "Shoe sizing (optional)" }),
1032
- /* @__PURE__ */ jsx(InputRow, { label: "Foot length", fieldKey: "footLengthCm", placeholder: isCm ? "e.g. 27" : "e.g. 10.5", type: "number", unit: sizingUnit }),
1033
- /* @__PURE__ */ jsx(InputRow, { label: shoeField.label, fieldKey: shoeField.key, placeholder: shoeField.ph })
1405
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-optional-fields", style: { display: "none" }, children: allOptFields.map(renderField) })
1034
1406
  ] })
1035
1407
  ] });
1036
1408
  })() }) : /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1071,61 +1443,37 @@ function PrimeStyleTryonInner({
1071
1443
  if (saveToggle && saveFormName.trim()) {
1072
1444
  saveProfile(saveFormName.trim());
1073
1445
  }
1074
- handleSubmit();
1446
+ setSizingLoading(true);
1447
+ submitSizing();
1448
+ setView("size-result");
1075
1449
  }, children: [
1076
- "Get My Size & Try On ",
1450
+ "Get My Size ",
1077
1451
  /* @__PURE__ */ jsx(ArrowRightIcon, {})
1078
1452
  ] })
1079
1453
  ] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}-${formKey}`);
1080
1454
  }
1081
- function ProcessingView() {
1082
- const barCb = useCallback((el) => {
1083
- progressBarRef.current = el;
1084
- if (el) el.style.width = `${Math.round(progressRef.current)}%`;
1085
- }, []);
1086
- const pctCb = useCallback((el) => {
1087
- progressTextRef.current = el;
1088
- if (el) el.textContent = `${Math.round(progressRef.current)}%`;
1089
- }, []);
1090
- const statusCb = useCallback((el) => {
1091
- progressStatusRef.current = el;
1092
- }, []);
1093
- return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
1094
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing-image-wrap", children: [
1095
- previewUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
1096
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-processing-blur", style: { backgroundImage: `url(${previewUrl})` } }),
1097
- /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "Your photo", className: "ps-tryon-processing-model" })
1098
- ] }),
1099
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-line" }),
1100
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-overlay" })
1101
- ] }),
1102
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-section", children: [
1103
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-progress-bar-wrap", children: /* @__PURE__ */ jsx("div", { ref: barCb, className: "ps-tryon-progress-bar-fill" }) }),
1104
- /* @__PURE__ */ jsx("span", { ref: pctCb, className: "ps-tryon-progress-pct", children: "0%" })
1105
- ] }),
1106
- /* @__PURE__ */ jsx("div", { ref: statusCb, className: cx("ps-tryon-processing-text", cn.processingText), children: "Preparing your image..." }),
1107
- /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-sub", cn.processingSubText), children: "This usually takes 15-25 seconds" })
1108
- ] });
1109
- }
1110
- function ResultView() {
1111
- const hasSizing = !!sizingResult || sizingLoading;
1112
- const hasBoth = !!resultImageUrl && hasSizing;
1113
- const [profileName, setProfileName] = useState("");
1455
+ function SizeResultView() {
1114
1456
  const [showFitDetails, setShowFitDetails] = useState(false);
1115
1457
  const confidenceLabel = sizingResult?.confidence === "high" ? "High Confidence" : sizingResult?.confidence === "medium" ? "Medium Confidence" : sizingResult?.confidence === "low" ? "Low Confidence" : "";
1116
- return /* @__PURE__ */ jsxs("div", { className: `ps-tryon-result-layout${hasBoth ? " ps-tryon-result-split" : ""}`, children: [
1117
- resultImageUrl && /* @__PURE__ */ jsx("div", { className: "ps-tryon-result-image-col", children: /* @__PURE__ */ jsx("img", { src: resultImageUrl, alt: "Try-on result", className: cn.resultImage }) }),
1118
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-result-info-col", children: [
1119
- sizingLoading && !sizingResult && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-recommend ps-tryon-sizing-loading", children: [
1120
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-loading-spinner" }),
1121
- /* @__PURE__ */ jsx("p", { className: "ps-tryon-size-reasoning", children: "Analyzing your size..." })
1122
- ] }),
1123
- sizingResult && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-recommend", children: [
1124
- /* @__PURE__ */ jsx("h3", { className: "ps-tryon-size-title", children: "Recommended Size" }),
1458
+ return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-result-view", children: [
1459
+ sizingLoading && !sizingResult && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-recommend ps-tryon-sizing-loading", children: [
1460
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-loading-spinner" }),
1461
+ /* @__PURE__ */ jsx("p", { className: "ps-tryon-size-reasoning", children: "Analyzing your size..." })
1462
+ ] }),
1463
+ sizingResult && /* @__PURE__ */ jsxs(Fragment, { children: [
1464
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-recommend", children: [
1465
+ /* @__PURE__ */ jsx("h3", { className: "ps-tryon-size-title", children: "Your Size" }),
1125
1466
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-hero-row", children: [
1126
1467
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-badge", children: sizingResult.recommendedSize }),
1127
1468
  /* @__PURE__ */ jsx("span", { className: `ps-tryon-size-conf-label ps-conf-${sizingResult.confidence}`, children: confidenceLabel })
1128
1469
  ] }),
1470
+ sizingResult.sections && Object.keys(sizingResult.sections).length > 1 && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-multi-section", children: [
1471
+ /* @__PURE__ */ jsx("h4", { className: "ps-tryon-fit-summary-title", children: "Sizing by Garment" }),
1472
+ Object.entries(sizingResult.sections).map(([name, sec]) => /* @__PURE__ */ jsxs("div", { className: "ps-tryon-section-row", children: [
1473
+ /* @__PURE__ */ jsx("span", { className: "ps-tryon-section-name", children: name }),
1474
+ /* @__PURE__ */ jsx("span", { className: "ps-tryon-size-badge ps-tryon-section-badge", children: sec.recommendedSize })
1475
+ ] }, name))
1476
+ ] }),
1129
1477
  sizingResult.matchDetails && sizingResult.matchDetails.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-fit-summary", children: [
1130
1478
  /* @__PURE__ */ jsx("h4", { className: "ps-tryon-fit-summary-title", children: "Fit Summary" }),
1131
1479
  sizingResult.matchDetails.map((m, i) => /* @__PURE__ */ jsxs("div", { className: "ps-tryon-fit-row", children: [
@@ -1136,7 +1484,7 @@ function PrimeStyleTryonInner({
1136
1484
  m.fit === "good" ? "within range" : m.fit === "tight" ? "may be snug" : "may be loose"
1137
1485
  ] })
1138
1486
  ] }, i)),
1139
- /* @__PURE__ */ jsx("button", { className: "ps-tryon-fit-details-toggle", onClick: () => setShowFitDetails(!showFitDetails), children: showFitDetails ? "Hide detailed fit analysis ↑" : "See detailed fit analysis ↓" }),
1487
+ /* @__PURE__ */ jsx("button", { className: "ps-tryon-fit-details-toggle", onClick: () => setShowFitDetails(!showFitDetails), children: showFitDetails ? "Hide details ↑" : "See details ↓" }),
1140
1488
  showFitDetails && /* @__PURE__ */ jsxs("table", { className: "ps-tryon-fit-table", children: [
1141
1489
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1142
1490
  /* @__PURE__ */ jsx("th", { children: "Area" }),
@@ -1161,35 +1509,56 @@ function PrimeStyleTryonInner({
1161
1509
  ] }),
1162
1510
  (!sizingResult.matchDetails || sizingResult.matchDetails.length === 0) && sizingResult.reasoning && /* @__PURE__ */ jsx("p", { className: "ps-tryon-size-reasoning", children: sizingResult.reasoning })
1163
1511
  ] }),
1512
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-tryon-cta", children: [
1513
+ /* @__PURE__ */ jsxs("button", { className: "ps-tryon-cta", onClick: () => setView("upload"), children: [
1514
+ "See how it looks on you ",
1515
+ /* @__PURE__ */ jsx(ArrowRightIcon, {})
1516
+ ] }),
1517
+ /* @__PURE__ */ jsx("button", { className: "ps-tryon-btn-secondary", onClick: handleClose, children: "Done" })
1518
+ ] })
1519
+ ] })
1520
+ ] });
1521
+ }
1522
+ function ProcessingView() {
1523
+ const barCb = useCallback((el) => {
1524
+ progressBarRef.current = el;
1525
+ if (el) el.style.width = `${Math.round(progressRef.current)}%`;
1526
+ }, []);
1527
+ const pctCb = useCallback((el) => {
1528
+ progressTextRef.current = el;
1529
+ if (el) el.textContent = `${Math.round(progressRef.current)}%`;
1530
+ }, []);
1531
+ const statusCb = useCallback((el) => {
1532
+ progressStatusRef.current = el;
1533
+ }, []);
1534
+ return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
1535
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing-image-wrap", children: [
1536
+ previewUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
1537
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-processing-blur", style: { backgroundImage: `url(${previewUrl})` } }),
1538
+ /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "Your photo", className: "ps-tryon-processing-model" })
1539
+ ] }),
1540
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-line" }),
1541
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-overlay" })
1542
+ ] }),
1543
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-section", children: [
1544
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-progress-bar-wrap", children: /* @__PURE__ */ jsx("div", { ref: barCb, className: "ps-tryon-progress-bar-fill" }) }),
1545
+ /* @__PURE__ */ jsx("span", { ref: pctCb, className: "ps-tryon-progress-pct", children: "0%" })
1546
+ ] }),
1547
+ /* @__PURE__ */ jsx("div", { ref: statusCb, className: cx("ps-tryon-processing-text", cn.processingText), children: "Preparing your image..." }),
1548
+ /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-sub", cn.processingSubText), children: "This usually takes 15-25 seconds" })
1549
+ ] });
1550
+ }
1551
+ function ResultView() {
1552
+ return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-result-layout", children: [
1553
+ resultImageUrl && /* @__PURE__ */ jsx("div", { className: "ps-tryon-result-image-col", children: /* @__PURE__ */ jsx("img", { src: resultImageUrl, alt: "Try-on result", className: cn.resultImage }) }),
1554
+ /* @__PURE__ */ jsxs("div", { className: "ps-tryon-result-info-col", children: [
1555
+ sizingResult && /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-recommend ps-tryon-size-compact", children: /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-hero-row", children: [
1556
+ /* @__PURE__ */ jsx("span", { className: "ps-tryon-size-compact-label", children: "Your size:" }),
1557
+ /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-badge", children: sizingResult.recommendedSize })
1558
+ ] }) }),
1164
1559
  /* @__PURE__ */ jsxs("div", { className: cx("ps-tryon-result-actions", cn.resultActions), children: [
1165
1560
  /* @__PURE__ */ jsx("button", { onClick: handleDownload, className: cx("ps-tryon-btn-download", cn.downloadButton), children: "Download" }),
1166
- /* @__PURE__ */ jsx("button", { onClick: handleRetry, className: cx("ps-tryon-btn-retry", cn.retryButton), children: "Try Another" })
1167
- ] }),
1168
- sizingMethod && !profileSaved && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-prompt", children: [
1169
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-save-label", children: "Save your measurements for next time" }),
1170
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-row", children: [
1171
- /* @__PURE__ */ jsx("input", { type: "text", placeholder: "Name this profile (e.g. John, Sarah)", value: profileName, onChange: (e) => setProfileName(e.target.value) }),
1172
- activeProfileId ? /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-btn-group", children: [
1173
- /* @__PURE__ */ jsxs("button", { onClick: () => {
1174
- if (profileName.trim()) saveProfile(profileName.trim());
1175
- }, children: [
1176
- "💾",
1177
- " Update"
1178
- ] }),
1179
- /* @__PURE__ */ jsx("button", { className: "ps-tryon-save-new-btn", onClick: () => {
1180
- if (profileName.trim()) saveProfile(profileName.trim(), true);
1181
- }, children: "+ New" })
1182
- ] }) : /* @__PURE__ */ jsxs("button", { onClick: () => {
1183
- if (profileName.trim()) saveProfile(profileName.trim());
1184
- }, children: [
1185
- "💾",
1186
- " Save"
1187
- ] })
1188
- ] })
1189
- ] }),
1190
- profileSaved && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-done", children: [
1191
- /* @__PURE__ */ jsx(CheckIcon, {}),
1192
- " Measurements saved to profile"
1561
+ /* @__PURE__ */ jsx("button", { onClick: handleRetry, className: cx("ps-tryon-btn-retry", cn.retryButton), children: "Start Over" })
1193
1562
  ] })
1194
1563
  ] })
1195
1564
  ] });
@@ -1303,12 +1672,14 @@ function PrimeStyleTryonInner({
1303
1672
  switch (view) {
1304
1673
  case "welcome":
1305
1674
  return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(WelcomeView, {}) }, "v-welcome");
1306
- case "upload":
1307
- return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(UploadView, {}) }, "v-upload");
1308
1675
  case "sizing-choice":
1309
1676
  return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(SizingChoiceView, {}) }, "v-choice");
1310
1677
  case "sizing-form":
1311
1678
  return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(SizingFormView, {}) }, "v-form");
1679
+ case "size-result":
1680
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(SizeResultView, {}) }, "v-sizeresult");
1681
+ case "upload":
1682
+ return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(UploadView, {}) }, "v-upload");
1312
1683
  case "processing":
1313
1684
  return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(ProcessingView, {}) }, "v-proc");
1314
1685
  case "result":
@@ -1742,8 +2113,27 @@ const STYLES = `
1742
2113
  .ps-tryon-equiv-region { padding: clamp(4px, 0.36vw, 7px) clamp(6px, 0.52vw, 10px); font-size: clamp(9px, 0.63vw, 12px); color: #999; font-weight: 600; background: rgba(255,255,255,0.03); }
1743
2114
  .ps-tryon-equiv-value { padding: clamp(4px, 0.36vw, 7px) clamp(7px, 0.63vw, 12px); font-size: clamp(10px, 0.73vw, 14px); color: #fff; font-weight: 700; }
1744
2115
 
2116
+ /* Multi-section garment sizing (tuxedo/set) */
2117
+ .ps-tryon-multi-section { margin-bottom: clamp(10px, 0.83vw, 16px); }
2118
+ .ps-tryon-section-row { display: flex; align-items: center; justify-content: space-between; padding: clamp(6px, 0.52vw, 10px) 0; border-bottom: 1px solid #222; }
2119
+ .ps-tryon-section-row:last-child { border-bottom: none; }
2120
+ .ps-tryon-section-name { font-size: clamp(11px, 0.78vw, 15px); color: #ccc; font-weight: 500; }
2121
+ .ps-tryon-section-badge { font-size: clamp(12px, 0.83vw, 16px); min-width: auto; padding: clamp(3px, 0.26vw, 5px) clamp(10px, 0.83vw, 16px); }
2122
+
1745
2123
  .ps-tryon-size-reasoning { font-size: clamp(10px, 0.73vw, 14px); color: #ccc; line-height: 1.6; margin-bottom: clamp(8px, 0.73vw, 14px); }
1746
2124
 
2125
+ /* Size Result View (standalone sizing result before try-on) */
2126
+ .ps-tryon-size-result-view { display: flex; flex-direction: column; gap: clamp(12px, 1vw, 20px); }
2127
+ .ps-tryon-tryon-cta { display: flex; flex-direction: column; gap: clamp(8px, 0.7vw, 12px); margin-top: clamp(8px, 0.7vw, 14px); }
2128
+ .ps-tryon-btn-secondary {
2129
+ background: transparent; border: 1.5px solid #444; color: #999; font-size: clamp(11px, 0.83vw, 15px);
2130
+ font-weight: 600; padding: clamp(8px, 0.7vw, 12px) clamp(16px, 1.3vw, 24px); border-radius: clamp(6px, 0.52vw, 10px);
2131
+ cursor: pointer; transition: all 0.2s;
2132
+ }
2133
+ .ps-tryon-btn-secondary:hover { border-color: #666; color: #ccc; }
2134
+ .ps-tryon-size-compact { padding: clamp(6px, 0.5vw, 10px) 0; }
2135
+ .ps-tryon-size-compact-label { font-size: clamp(11px, 0.78vw, 14px); color: #999; font-weight: 500; }
2136
+
1747
2137
  /* Save profile prompt */
1748
2138
  .ps-tryon-save-prompt { margin-top: clamp(8px, 0.73vw, 14px); padding: clamp(8px, 0.73vw, 14px); border: 1px solid #333; border-radius: clamp(8px, 0.63vw, 12px); background: #1a1b1a; }
1749
2139
  .ps-tryon-save-label { font-size: clamp(9px, 0.63vw, 12px); color: #999; margin-bottom: clamp(6px, 0.52vw, 10px); }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Header alias mapping — maps common size guide column headers
3
+ * to standardised measurement field keys.
4
+ */
5
+ export declare const HEADER_ALIASES: Record<string, string>;
6
+ /** Human-readable labels for each measurement key */
7
+ export declare const FIELD_LABELS: Record<string, string>;
8
+ /** Placeholder values shown in form inputs */
9
+ export declare const FIELD_PLACEHOLDERS: Record<string, string>;
10
+ /** Regex to detect size label strings (S/M/L/XL or numeric 28-52) */
11
+ export declare const SIZE_LABEL_PATTERN: RegExp;
12
+ /** Keys that are shoe-related */
13
+ export declare const SHOE_FIELD_KEYS: Set<string>;
@@ -0,0 +1,35 @@
1
+ export interface SizeGuideField {
2
+ key: string;
3
+ label: string;
4
+ required: boolean;
5
+ unit: "cm" | "size" | string;
6
+ placeholder?: string;
7
+ category: "body" | "shoe" | "other";
8
+ }
9
+ export interface NormalizedSection {
10
+ headers: string[];
11
+ rows: string[][];
12
+ requiredFields: SizeGuideField[];
13
+ }
14
+ export interface NormalizedSizeGuide {
15
+ found: true;
16
+ title?: string;
17
+ headers: string[];
18
+ rows: string[][];
19
+ requiredFields: SizeGuideField[];
20
+ sections?: Record<string, NormalizedSection>;
21
+ }
22
+ export type NormalizerResult = {
23
+ success: true;
24
+ data: NormalizedSizeGuide;
25
+ } | {
26
+ success: false;
27
+ reason: string;
28
+ };
29
+ export declare function deriveRequiredFields(headers: string[]): SizeGuideField[];
30
+ /**
31
+ * Attempts to normalise `sizeGuideData` (any format) into a structured
32
+ * `NormalizedSizeGuide`. Returns `{ success: false }` for unrecognised
33
+ * formats (HTML, images, free-form text) so the caller can fall back to AI.
34
+ */
35
+ export declare function normalizeSizeGuide(input: unknown): NormalizerResult;
package/dist/types.d.ts CHANGED
@@ -137,6 +137,53 @@ export interface SizeGuideData {
137
137
  headers: string[];
138
138
  rows: string[][];
139
139
  }
140
+ /** A measurement field descriptor used by the sizing form */
141
+ export interface SizeGuideField {
142
+ key: string;
143
+ label: string;
144
+ required: boolean;
145
+ unit: "cm" | "size" | string;
146
+ placeholder?: string;
147
+ category: "body" | "shoe" | "other";
148
+ }
149
+ /** A normalised section within a multi-garment size guide (e.g. tuxedo) */
150
+ export interface NormalizedSection {
151
+ headers: string[];
152
+ rows: string[][];
153
+ requiredFields: SizeGuideField[];
154
+ }
155
+ /** Fully normalised size guide ready for deterministic comparison */
156
+ export interface NormalizedSizeGuide {
157
+ found: true;
158
+ title?: string;
159
+ headers: string[];
160
+ rows: string[][];
161
+ requiredFields: SizeGuideField[];
162
+ sections?: Record<string, NormalizedSection>;
163
+ }
164
+ /** Match detail for a single measurement in a sizing recommendation */
165
+ export interface MatchDetail {
166
+ measurement: string;
167
+ userValue: string;
168
+ chartRange: string;
169
+ fit: "good" | "tight" | "loose";
170
+ }
171
+ /** Per-section recommendation result (for multi-garment products) */
172
+ export interface SectionRecommendation {
173
+ recommendedSize: string;
174
+ matchDetails: MatchDetail[];
175
+ }
176
+ /** Full sizing recommendation result */
177
+ export interface SizingResult {
178
+ recommendedSize: string;
179
+ recommendedLength?: string | null;
180
+ confidence: "high" | "medium" | "low" | string;
181
+ reasoning: string;
182
+ internationalSizes?: Record<string, string>;
183
+ matchDetails?: MatchDetail[];
184
+ method?: "deterministic" | "ai";
185
+ sections?: Record<string, SectionRecommendation>;
186
+ }
140
187
  /** Custom events emitted by the component */
141
188
  export interface PrimeStyleEvents {
142
189
  "ps:open": CustomEvent<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "3.6.6",
3
+ "version": "3.8.0",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",