@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.
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +526 -136
- package/dist/sizing/constants.d.ts +13 -0
- package/dist/sizing/normalizer.d.ts +35 -0
- package/dist/types.d.ts +47 -0
- package/package.json +1 -1
package/dist/react/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface PrimeStyleTryonProps {
|
|
|
27
27
|
message: string;
|
|
28
28
|
code?: string;
|
|
29
29
|
}) => void;
|
|
30
|
-
/** Size guide data
|
|
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;
|
package/dist/react/index.js
CHANGED
|
@@ -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", "
|
|
55
|
-
const TOTAL_STEPS =
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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("
|
|
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" &&
|
|
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: "
|
|
804
|
-
/* @__PURE__ */ jsx("p", { className: "ps-tryon-welcome-sub", children: "
|
|
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(
|
|
808
|
-
{ icon: /* @__PURE__ */ jsx(
|
|
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("
|
|
816
|
-
"
|
|
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:
|
|
830
|
-
"
|
|
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: "
|
|
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
|
-
|
|
1273
|
+
setView("upload");
|
|
913
1274
|
}, children: [
|
|
914
|
-
/* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-icon", children: /* @__PURE__ */ jsx(
|
|
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
|
|
917
|
-
/* @__PURE__ */ jsx("div", { className: "ps-tryon-choice-desc", children: "
|
|
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
|
-
|
|
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
|
-
|
|
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__ */
|
|
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
|
-
|
|
1446
|
+
setSizingLoading(true);
|
|
1447
|
+
submitSizing();
|
|
1448
|
+
setView("size-result");
|
|
1075
1449
|
}, children: [
|
|
1076
|
-
"Get My Size
|
|
1450
|
+
"Get My Size ",
|
|
1077
1451
|
/* @__PURE__ */ jsx(ArrowRightIcon, {})
|
|
1078
1452
|
] })
|
|
1079
1453
|
] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}-${formKey}`);
|
|
1080
1454
|
}
|
|
1081
|
-
function
|
|
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:
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
|
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: "
|
|
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>;
|