@primestyleai/tryon 3.7.0 → 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
  ] })
@@ -1082,61 +1443,37 @@ function PrimeStyleTryonInner({
1082
1443
  if (saveToggle && saveFormName.trim()) {
1083
1444
  saveProfile(saveFormName.trim());
1084
1445
  }
1085
- handleSubmit();
1446
+ setSizingLoading(true);
1447
+ submitSizing();
1448
+ setView("size-result");
1086
1449
  }, children: [
1087
- "Get My Size & Try On ",
1450
+ "Get My Size ",
1088
1451
  /* @__PURE__ */ jsx(ArrowRightIcon, {})
1089
1452
  ] })
1090
1453
  ] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}-${formKey}`);
1091
1454
  }
1092
- function ProcessingView() {
1093
- const barCb = useCallback((el) => {
1094
- progressBarRef.current = el;
1095
- if (el) el.style.width = `${Math.round(progressRef.current)}%`;
1096
- }, []);
1097
- const pctCb = useCallback((el) => {
1098
- progressTextRef.current = el;
1099
- if (el) el.textContent = `${Math.round(progressRef.current)}%`;
1100
- }, []);
1101
- const statusCb = useCallback((el) => {
1102
- progressStatusRef.current = el;
1103
- }, []);
1104
- return /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing", children: [
1105
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-processing-image-wrap", children: [
1106
- previewUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
1107
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-processing-blur", style: { backgroundImage: `url(${previewUrl})` } }),
1108
- /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "Your photo", className: "ps-tryon-processing-model" })
1109
- ] }),
1110
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-line" }),
1111
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-scan-overlay" })
1112
- ] }),
1113
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-progress-section", children: [
1114
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-progress-bar-wrap", children: /* @__PURE__ */ jsx("div", { ref: barCb, className: "ps-tryon-progress-bar-fill" }) }),
1115
- /* @__PURE__ */ jsx("span", { ref: pctCb, className: "ps-tryon-progress-pct", children: "0%" })
1116
- ] }),
1117
- /* @__PURE__ */ jsx("div", { ref: statusCb, className: cx("ps-tryon-processing-text", cn.processingText), children: "Preparing your image..." }),
1118
- /* @__PURE__ */ jsx("p", { className: cx("ps-tryon-processing-sub", cn.processingSubText), children: "This usually takes 15-25 seconds" })
1119
- ] });
1120
- }
1121
- function ResultView() {
1122
- const hasSizing = !!sizingResult || sizingLoading;
1123
- const hasBoth = !!resultImageUrl && hasSizing;
1124
- const [profileName, setProfileName] = useState("");
1455
+ function SizeResultView() {
1125
1456
  const [showFitDetails, setShowFitDetails] = useState(false);
1126
1457
  const confidenceLabel = sizingResult?.confidence === "high" ? "High Confidence" : sizingResult?.confidence === "medium" ? "Medium Confidence" : sizingResult?.confidence === "low" ? "Low Confidence" : "";
1127
- return /* @__PURE__ */ jsxs("div", { className: `ps-tryon-result-layout${hasBoth ? " ps-tryon-result-split" : ""}`, children: [
1128
- resultImageUrl && /* @__PURE__ */ jsx("div", { className: "ps-tryon-result-image-col", children: /* @__PURE__ */ jsx("img", { src: resultImageUrl, alt: "Try-on result", className: cn.resultImage }) }),
1129
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-result-info-col", children: [
1130
- sizingLoading && !sizingResult && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-recommend ps-tryon-sizing-loading", children: [
1131
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-loading-spinner" }),
1132
- /* @__PURE__ */ jsx("p", { className: "ps-tryon-size-reasoning", children: "Analyzing your size..." })
1133
- ] }),
1134
- sizingResult && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-recommend", children: [
1135
- /* @__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" }),
1136
1466
  /* @__PURE__ */ jsxs("div", { className: "ps-tryon-size-hero-row", children: [
1137
1467
  /* @__PURE__ */ jsx("div", { className: "ps-tryon-size-badge", children: sizingResult.recommendedSize }),
1138
1468
  /* @__PURE__ */ jsx("span", { className: `ps-tryon-size-conf-label ps-conf-${sizingResult.confidence}`, children: confidenceLabel })
1139
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
+ ] }),
1140
1477
  sizingResult.matchDetails && sizingResult.matchDetails.length > 0 && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-fit-summary", children: [
1141
1478
  /* @__PURE__ */ jsx("h4", { className: "ps-tryon-fit-summary-title", children: "Fit Summary" }),
1142
1479
  sizingResult.matchDetails.map((m, i) => /* @__PURE__ */ jsxs("div", { className: "ps-tryon-fit-row", children: [
@@ -1147,7 +1484,7 @@ function PrimeStyleTryonInner({
1147
1484
  m.fit === "good" ? "within range" : m.fit === "tight" ? "may be snug" : "may be loose"
1148
1485
  ] })
1149
1486
  ] }, i)),
1150
- /* @__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 ↓" }),
1151
1488
  showFitDetails && /* @__PURE__ */ jsxs("table", { className: "ps-tryon-fit-table", children: [
1152
1489
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1153
1490
  /* @__PURE__ */ jsx("th", { children: "Area" }),
@@ -1172,35 +1509,56 @@ function PrimeStyleTryonInner({
1172
1509
  ] }),
1173
1510
  (!sizingResult.matchDetails || sizingResult.matchDetails.length === 0) && sizingResult.reasoning && /* @__PURE__ */ jsx("p", { className: "ps-tryon-size-reasoning", children: sizingResult.reasoning })
1174
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
+ ] }) }),
1175
1559
  /* @__PURE__ */ jsxs("div", { className: cx("ps-tryon-result-actions", cn.resultActions), children: [
1176
1560
  /* @__PURE__ */ jsx("button", { onClick: handleDownload, className: cx("ps-tryon-btn-download", cn.downloadButton), children: "Download" }),
1177
- /* @__PURE__ */ jsx("button", { onClick: handleRetry, className: cx("ps-tryon-btn-retry", cn.retryButton), children: "Try Another" })
1178
- ] }),
1179
- sizingMethod && !profileSaved && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-prompt", children: [
1180
- /* @__PURE__ */ jsx("div", { className: "ps-tryon-save-label", children: "Save your measurements for next time" }),
1181
- /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-row", children: [
1182
- /* @__PURE__ */ jsx("input", { type: "text", placeholder: "Name this profile (e.g. John, Sarah)", value: profileName, onChange: (e) => setProfileName(e.target.value) }),
1183
- activeProfileId ? /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-btn-group", children: [
1184
- /* @__PURE__ */ jsxs("button", { onClick: () => {
1185
- if (profileName.trim()) saveProfile(profileName.trim());
1186
- }, children: [
1187
- "💾",
1188
- " Update"
1189
- ] }),
1190
- /* @__PURE__ */ jsx("button", { className: "ps-tryon-save-new-btn", onClick: () => {
1191
- if (profileName.trim()) saveProfile(profileName.trim(), true);
1192
- }, children: "+ New" })
1193
- ] }) : /* @__PURE__ */ jsxs("button", { onClick: () => {
1194
- if (profileName.trim()) saveProfile(profileName.trim());
1195
- }, children: [
1196
- "💾",
1197
- " Save"
1198
- ] })
1199
- ] })
1200
- ] }),
1201
- profileSaved && /* @__PURE__ */ jsxs("div", { className: "ps-tryon-save-done", children: [
1202
- /* @__PURE__ */ jsx(CheckIcon, {}),
1203
- " Measurements saved to profile"
1561
+ /* @__PURE__ */ jsx("button", { onClick: handleRetry, className: cx("ps-tryon-btn-retry", cn.retryButton), children: "Start Over" })
1204
1562
  ] })
1205
1563
  ] })
1206
1564
  ] });
@@ -1314,12 +1672,14 @@ function PrimeStyleTryonInner({
1314
1672
  switch (view) {
1315
1673
  case "welcome":
1316
1674
  return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(WelcomeView, {}) }, "v-welcome");
1317
- case "upload":
1318
- return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(UploadView, {}) }, "v-upload");
1319
1675
  case "sizing-choice":
1320
1676
  return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(SizingChoiceView, {}) }, "v-choice");
1321
1677
  case "sizing-form":
1322
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");
1323
1683
  case "processing":
1324
1684
  return /* @__PURE__ */ jsx("div", { className: "ps-tryon-view-enter", children: /* @__PURE__ */ jsx(ProcessingView, {}) }, "v-proc");
1325
1685
  case "result":
@@ -1753,8 +2113,27 @@ const STYLES = `
1753
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); }
1754
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; }
1755
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
+
1756
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); }
1757
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
+
1758
2137
  /* Save profile prompt */
1759
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; }
1760
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.7.0",
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",