@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.
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +501 -122
- 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
|
] })
|
|
@@ -1082,61 +1443,37 @@ function PrimeStyleTryonInner({
|
|
|
1082
1443
|
if (saveToggle && saveFormName.trim()) {
|
|
1083
1444
|
saveProfile(saveFormName.trim());
|
|
1084
1445
|
}
|
|
1085
|
-
|
|
1446
|
+
setSizingLoading(true);
|
|
1447
|
+
submitSizing();
|
|
1448
|
+
setView("size-result");
|
|
1086
1449
|
}, children: [
|
|
1087
|
-
"Get My Size
|
|
1450
|
+
"Get My Size ",
|
|
1088
1451
|
/* @__PURE__ */ jsx(ArrowRightIcon, {})
|
|
1089
1452
|
] })
|
|
1090
1453
|
] }, `form-${formGender}-${sizingUnit}-${heightUnit}-${sizingCountry}-${formKey}`);
|
|
1091
1454
|
}
|
|
1092
|
-
function
|
|
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:
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
|
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: "
|
|
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>;
|