@primestyleai/tryon 5.10.106 → 5.10.107
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.
|
@@ -9256,1564 +9256,190 @@ const locales = {
|
|
|
9256
9256
|
for (const [code, translations] of Object.entries(locales)) {
|
|
9257
9257
|
registerLocale(code, translations);
|
|
9258
9258
|
}
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
var jsxRuntimeExports = jsxRuntime.exports;
|
|
9287
|
-
const STORAGE_KEY = "ps_session";
|
|
9288
|
-
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
9289
|
-
const FALLBACK_PREFIX = "ps_mem_";
|
|
9290
|
-
let memoryFallback = null;
|
|
9291
|
-
function hasLocalStorage() {
|
|
9292
|
-
try {
|
|
9293
|
-
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
9294
|
-
} catch {
|
|
9295
|
-
return false;
|
|
9259
|
+
const STYLES$1 = `
|
|
9260
|
+
/* Variable defaults must live on BOTH the root (for the trigger button)
|
|
9261
|
+
and the overlay (which is React-portaled to <body> and therefore not
|
|
9262
|
+
a descendant of .ps-tryon-root, so the cascade is broken). Without
|
|
9263
|
+
this duplication the slider track gradient and the thumb border —
|
|
9264
|
+
both var(--ps-accent) — render as unset and become invisible. */
|
|
9265
|
+
.ps-tryon-root,
|
|
9266
|
+
.ps-tryon-overlay {
|
|
9267
|
+
--ps-accent: #3B82F6;
|
|
9268
|
+
--ps-accent-hover: #2563EB;
|
|
9269
|
+
--ps-accent-light: #BFDBFE;
|
|
9270
|
+
--ps-text-primary: #1C1D1E;
|
|
9271
|
+
--ps-text-secondary: #454545;
|
|
9272
|
+
--ps-text-muted: #84898C;
|
|
9273
|
+
--ps-text-dim: #696E71;
|
|
9274
|
+
--ps-border-color: #E7E7E7;
|
|
9275
|
+
--ps-border-subtle: #F0F0F0;
|
|
9276
|
+
--ps-bg-primary: #FFFFFF;
|
|
9277
|
+
--ps-bg-secondary: #F8FAFF;
|
|
9278
|
+
--ps-bg-tertiary: #F0F0F0;
|
|
9279
|
+
--ps-input-bg: #FFFFFF;
|
|
9280
|
+
--ps-input-border: #E7E7E7;
|
|
9281
|
+
--ps-input-focus-border: var(--ps-accent);
|
|
9282
|
+
--ps-error-color: #E7000B;
|
|
9283
|
+
--ps-success-color: #1C9D4C;
|
|
9284
|
+
--ps-warning-color: #DF8400;
|
|
9285
|
+
--ps-logo-height: 2.8vw;
|
|
9296
9286
|
}
|
|
9297
|
-
}
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9287
|
+
.ps-tryon-root { display: inline-block; visibility: visible !important; }
|
|
9288
|
+
|
|
9289
|
+
.ps-tryon-btn {
|
|
9290
|
+
display: inline-flex; align-items: center; gap: max(6px, 0.42vw);
|
|
9291
|
+
padding: var(--ps-btn-padding, max(10px, 0.63vw) max(16px, 1.25vw));
|
|
9292
|
+
background: var(--ps-btn-bg, var(--ps-accent)); color: var(--ps-btn-color, #FFFFFF);
|
|
9293
|
+
font-family: var(--ps-btn-font, system-ui, -apple-system, sans-serif);
|
|
9294
|
+
font-size: var(--ps-btn-font-size, max(13px, 0.73vw)); font-weight: var(--ps-btn-font-weight, 600);
|
|
9295
|
+
border: var(--ps-btn-border, none); border-radius: var(--ps-btn-radius, max(6px, 0.42vw));
|
|
9296
|
+
cursor: pointer; transition: all 0.2s ease;
|
|
9297
|
+
width: var(--ps-btn-width, auto); height: var(--ps-btn-height, auto);
|
|
9298
|
+
box-shadow: var(--ps-btn-shadow, none); line-height: 1; white-space: nowrap;
|
|
9299
|
+
box-sizing: border-box;
|
|
9301
9300
|
}
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
} catch {
|
|
9320
|
-
return null;
|
|
9301
|
+
.ps-tryon-btn svg { width: var(--ps-btn-icon-size, max(16px, 1.1vw)); height: var(--ps-btn-icon-size, max(16px, 1.1vw)); flex-shrink: 0; }
|
|
9302
|
+
.ps-tryon-btn:hover { background: var(--ps-btn-hover-bg, var(--ps-accent-hover)); transform: translateY(-1px); }
|
|
9303
|
+
.ps-tryon-btn:active { transform: translateY(0); }
|
|
9304
|
+
|
|
9305
|
+
.ps-tryon-overlay {
|
|
9306
|
+
position: fixed; inset: 0; background: var(--ps-modal-overlay, rgba(0,0,0,0.6));
|
|
9307
|
+
display: flex; align-items: center; justify-content: center;
|
|
9308
|
+
z-index: 2147483647;
|
|
9309
|
+
isolation: isolate;
|
|
9310
|
+
contain: layout style;
|
|
9311
|
+
padding: 0.83vw;
|
|
9312
|
+
padding: max(0.83vw, env(safe-area-inset-top)) max(0.83vw, env(safe-area-inset-right)) max(0.83vw, env(safe-area-inset-bottom)) max(0.83vw, env(safe-area-inset-left));
|
|
9313
|
+
animation: ps-fade-in 0.2s ease;
|
|
9314
|
+
overflow-y: auto; -webkit-overflow-scrolling: touch;
|
|
9315
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
9316
|
+
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
9317
|
+
box-sizing: border-box;
|
|
9321
9318
|
}
|
|
9322
|
-
}
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9319
|
+
.ps-tryon-overlay, .ps-tryon-overlay *, .ps-tryon-overlay *::before, .ps-tryon-overlay *::after { box-sizing: border-box; }
|
|
9320
|
+
@keyframes ps-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
9321
|
+
|
|
9322
|
+
.ps-tryon-modal {
|
|
9323
|
+
background: var(--ps-modal-bg, #FFFFFF); color: var(--ps-modal-color, #1C1D1E);
|
|
9324
|
+
border-radius: clamp(8px, 0.83vw, 16px); width: var(--ps-modal-width, 100%);
|
|
9325
|
+
max-width: var(--ps-modal-max-width, 36vw); max-height: 92dvh; overflow-y: auto;
|
|
9326
|
+
font-family: var(--ps-modal-font, system-ui, -apple-system, sans-serif);
|
|
9327
|
+
box-shadow: 0 8px 40px rgba(0,0,0,0.12); animation: ps-slide-up 0.3s ease;
|
|
9328
|
+
scrollbar-width: thin; scrollbar-color: var(--ps-border-color) transparent;
|
|
9329
|
+
flex-shrink: 0;
|
|
9330
|
+
container-type: inline-size;
|
|
9329
9331
|
}
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9332
|
+
.ps-tryon-modal-wide {
|
|
9333
|
+
/* Use the SMALLER of 55vw or 92dvh×(16/10) for width so the modal
|
|
9334
|
+
never gets wider than the height can support. Height bumped from
|
|
9335
|
+
70dvh → 82dvh to fit the longer accessory photo step (guidelines +
|
|
9336
|
+
legal notice + nav) without pushing content under the Analyze CTA. */
|
|
9337
|
+
width: min(55vw, calc(92dvh * 1.6));
|
|
9338
|
+
max-width: min(55vw, calc(92dvh * 1.6));
|
|
9339
|
+
height: min(82dvh, calc(55vw / 1.35));
|
|
9340
|
+
max-height: min(92dvh, calc(55vw / 1.35));
|
|
9341
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
9342
|
+
transition: height 0.45s cubic-bezier(0.32, 0.72, 0, 1), max-height 0.45s cubic-bezier(0.32, 0.72, 0, 1);
|
|
9339
9343
|
}
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
}
|
|
9344
|
-
function getDeviceHint() {
|
|
9345
|
-
if (typeof navigator === "undefined") return null;
|
|
9346
|
-
const ua2 = (navigator.userAgent || "").toLowerCase();
|
|
9347
|
-
if (/ipad|tablet|(android(?!.*mobile))/.test(ua2)) return "tablet";
|
|
9348
|
-
if (/mobile|android|iphone|ipod|blackberry|iemobile|opera mini/.test(ua2)) return "mobile";
|
|
9349
|
-
return "desktop";
|
|
9350
|
-
}
|
|
9351
|
-
const DEFAULT_API_URL = "http://localhost:4000";
|
|
9352
|
-
class ApiClient {
|
|
9353
|
-
constructor(apiKey, apiUrl) {
|
|
9354
|
-
this.apiKey = apiKey;
|
|
9355
|
-
this.baseUrl = (apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
9344
|
+
.ps-tryon-modal-tall.ps-tryon-modal-wide {
|
|
9345
|
+
height: min(92dvh, calc(55vw / 1.2));
|
|
9346
|
+
max-height: min(95dvh, calc(55vw / 1.2));
|
|
9356
9347
|
}
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9348
|
+
.ps-tryon-modal:has(.ps-tryon-drawer-open) { overflow: hidden; }
|
|
9349
|
+
@keyframes ps-slide-up { from { transform: translateY(12px) scale(0.97); opacity: 0; filter: blur(4px); } to { transform: none; opacity: 1; filter: none; } }
|
|
9350
|
+
|
|
9351
|
+
/* Header */
|
|
9352
|
+
.ps-tryon-header {
|
|
9353
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
9354
|
+
padding: max(8px, 0.7vw) max(12px, 1.25vw);
|
|
9355
|
+
background: var(--ps-modal-header-bg, rgba(255,255,255,0.95));
|
|
9356
|
+
border-bottom: 1px solid var(--ps-border-color);
|
|
9357
|
+
border-radius: max(8px, 0.83vw) max(8px, 0.83vw) 0 0;
|
|
9358
|
+
backdrop-filter: blur(12px); flex-shrink: 0;
|
|
9362
9359
|
}
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
garmentImage,
|
|
9366
|
-
// Attribution fields — backend logs these into the TryOnEvent collection
|
|
9367
|
-
// so non-technical operators can answer "which products are being tried on
|
|
9368
|
-
// and by how many customers". Safe fallbacks when absent.
|
|
9369
|
-
sessionId: getOrCreateSessionId(),
|
|
9370
|
-
deviceHint: getDeviceHint()
|
|
9371
|
-
};
|
|
9372
|
-
if (context?.modelImageId) body.modelImageId = context.modelImageId;
|
|
9373
|
-
if (modelImage) body.modelImage = modelImage;
|
|
9374
|
-
if (fitInfo && fitInfo.length > 0) body.fitInfo = fitInfo;
|
|
9375
|
-
if (category && category !== "apparel") body.category = category;
|
|
9376
|
-
if (context?.productId) body.productId = context.productId;
|
|
9377
|
-
if (context?.productTitle) body.productTitle = context.productTitle;
|
|
9378
|
-
if (context?.productDescription) body.productDescription = context.productDescription;
|
|
9379
|
-
if (context?.productMaterial) body.productMaterial = context.productMaterial;
|
|
9380
|
-
if (context?.silhouetteContext) body.silhouetteContext = context.silhouetteContext;
|
|
9381
|
-
if (context?.editFromPrevious) body.editFromPrevious = true;
|
|
9382
|
-
console.log("[ps-sdk:api] POST /api/v1/tryon payload", {
|
|
9383
|
-
modelImageBytes: modelImage.length,
|
|
9384
|
-
garmentImageBytes: garmentImage.length,
|
|
9385
|
-
category: category || "apparel",
|
|
9386
|
-
fitInfo: fitInfo?.map((f2) => ({
|
|
9387
|
-
area: f2.area,
|
|
9388
|
-
fit: f2.fit,
|
|
9389
|
-
userValue: f2.userValue,
|
|
9390
|
-
garmentRange: f2.garmentRange
|
|
9391
|
-
})) || null,
|
|
9392
|
-
silhouetteContext: context?.silhouetteContext || null
|
|
9393
|
-
});
|
|
9394
|
-
if (fitInfo && fitInfo.length > 0) {
|
|
9395
|
-
console.log("[ps-sdk:api] Body vs Garment (what Gemini will see):");
|
|
9396
|
-
for (const f2 of fitInfo) {
|
|
9397
|
-
const parts = (f2.garmentRange || "").replace(/[^\d.\-–]/g, " ").trim().split(/[\s\-–]+/).filter(Boolean).map(Number).filter((n2) => !isNaN(n2));
|
|
9398
|
-
let gap = "";
|
|
9399
|
-
if (parts.length && f2.userValue != null) {
|
|
9400
|
-
const mid = (Math.min(...parts) + Math.max(...parts)) / 2;
|
|
9401
|
-
const diff = Math.round((f2.userValue - mid) * 10) / 10;
|
|
9402
|
-
gap = diff > 0 ? `garment ${diff} smaller` : diff < 0 ? `garment ${Math.abs(diff)} larger` : "matched";
|
|
9403
|
-
}
|
|
9404
|
-
console.log(`[ps-sdk:api] ${f2.area.padEnd(10)} body=${f2.userValue ?? "?"} garment=${f2.garmentRange ?? "?"} → ${gap}`);
|
|
9405
|
-
}
|
|
9406
|
-
}
|
|
9407
|
-
const res = await fetch(`${this.baseUrl}/api/v1/tryon`, {
|
|
9408
|
-
method: "POST",
|
|
9409
|
-
headers: this.headers,
|
|
9410
|
-
body: JSON.stringify(body)
|
|
9411
|
-
});
|
|
9412
|
-
if (!res.ok) {
|
|
9413
|
-
const data = await res.json().catch(() => ({}));
|
|
9414
|
-
if (res.status === 402) {
|
|
9415
|
-
throw new PrimeStyleError(
|
|
9416
|
-
data.message || "Insufficient try-ons",
|
|
9417
|
-
"INSUFFICIENT_BALANCE"
|
|
9418
|
-
);
|
|
9419
|
-
}
|
|
9420
|
-
throw new PrimeStyleError(
|
|
9421
|
-
data.message || "Failed to submit try-on",
|
|
9422
|
-
"API_ERROR"
|
|
9423
|
-
);
|
|
9424
|
-
}
|
|
9425
|
-
return res.json();
|
|
9360
|
+
.ps-tryon-header-minimal {
|
|
9361
|
+
justify-content: flex-end; gap: 0.4vw; padding: 0.4vw 0.8vw;
|
|
9426
9362
|
}
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
const data = await res.json().catch(() => ({}));
|
|
9433
|
-
throw new PrimeStyleError(
|
|
9434
|
-
data.message || "Failed to get status",
|
|
9435
|
-
"API_ERROR"
|
|
9436
|
-
);
|
|
9437
|
-
}
|
|
9438
|
-
return res.json();
|
|
9363
|
+
.ps-tryon-back-btn {
|
|
9364
|
+
display: flex; align-items: center; gap: 0.35vw;
|
|
9365
|
+
background: none; border: none; color: var(--ps-text-secondary); cursor: pointer;
|
|
9366
|
+
font-size: 0.73vw; padding: 0; margin-bottom: 0.63vw;
|
|
9367
|
+
transition: color 0.2s;
|
|
9439
9368
|
}
|
|
9440
|
-
|
|
9441
|
-
|
|
9369
|
+
.ps-tryon-back-btn:hover { color: var(--ps-text-primary); }
|
|
9370
|
+
.ps-tryon-back-btn svg { width: 1vw; height: 0.63vw; stroke: currentColor; flex-shrink: 0; }
|
|
9371
|
+
|
|
9372
|
+
/* Regen progress bar — absolutely-positioned fill div inside the
|
|
9373
|
+
"Generating new try-on…" button. Animation runs at the browser's
|
|
9374
|
+
refresh rate (smooth) instead of via React state ticks. The button
|
|
9375
|
+
itself sets position: relative + overflow: hidden so the fill is
|
|
9376
|
+
clipped to the button's rounded shape. */
|
|
9377
|
+
@keyframes ps-tryon-regen-fill {
|
|
9378
|
+
from { width: 0%; }
|
|
9379
|
+
to { width: 100%; }
|
|
9442
9380
|
}
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9381
|
+
.ps-tryon-regen-fill {
|
|
9382
|
+
position: absolute;
|
|
9383
|
+
left: 0; top: 0; bottom: 0;
|
|
9384
|
+
width: 0%;
|
|
9385
|
+
background: rgba(255, 255, 255, 0.28);
|
|
9386
|
+
animation: ps-tryon-regen-fill 22s linear forwards;
|
|
9387
|
+
pointer-events: none;
|
|
9388
|
+
z-index: 0;
|
|
9449
9389
|
}
|
|
9450
|
-
}
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9390
|
+
.ps-tryon-title { display: flex; align-items: center; }
|
|
9391
|
+
.ps-tryon-logo-img { height: var(--ps-logo-height); width: auto; }
|
|
9392
|
+
.ps-tryon-header-actions { display: flex; align-items: center; gap: 0.42vw; }
|
|
9393
|
+
.ps-tryon-header-icon {
|
|
9394
|
+
/* Pure vw sizing collapsed to ~8 px on 375 px mobile. Clamp so the icon
|
|
9395
|
+
stays finger-tappable (min 30 px) while scaling up on large screens. */
|
|
9396
|
+
width: clamp(30px, 2.2vw, 34px); height: clamp(30px, 2.2vw, 34px);
|
|
9397
|
+
display: flex; align-items: center; justify-content: center;
|
|
9398
|
+
border: 1.5px solid var(--ps-border-color); border-radius: clamp(6px, 0.52vw, 10px); background: transparent;
|
|
9399
|
+
cursor: pointer; color: var(--ps-text-secondary); transition: all 0.2s;
|
|
9459
9400
|
}
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
this.emit(data.galleryId, data);
|
|
9467
|
-
} catch {
|
|
9468
|
-
}
|
|
9469
|
-
});
|
|
9470
|
-
this.eventSource.onopen = () => {
|
|
9471
|
-
this.reconnectAttempts = 0;
|
|
9472
|
-
};
|
|
9473
|
-
this.eventSource.onerror = () => {
|
|
9474
|
-
this.eventSource?.close();
|
|
9475
|
-
this.eventSource = null;
|
|
9476
|
-
this.scheduleReconnect();
|
|
9477
|
-
};
|
|
9401
|
+
.ps-tryon-header-icon:hover { border-color: var(--ps-accent); color: var(--ps-accent); }
|
|
9402
|
+
.ps-tryon-header-icon svg { stroke: currentColor; fill: none; width: clamp(14px, 0.9vw, 16px); height: clamp(14px, 0.9vw, 16px); }
|
|
9403
|
+
.ps-tryon-close {
|
|
9404
|
+
width: 2.2vw; height: 2.2vw; display: flex; align-items: center; justify-content: center;
|
|
9405
|
+
background: none; border: none; color: var(--ps-modal-close-color, #999);
|
|
9406
|
+
cursor: pointer; border-radius: 0.31vw; transition: background 0.15s;
|
|
9478
9407
|
}
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9408
|
+
.ps-tryon-close:hover { background: rgba(0,0,0,0.06); }
|
|
9409
|
+
.ps-tryon-close svg { width: 1vw; height: 1vw; }
|
|
9410
|
+
|
|
9411
|
+
/* Language switcher */
|
|
9412
|
+
.ps-tryon-lang-wrap { position: relative; z-index: 99999; }
|
|
9413
|
+
.ps-tryon-lang-trigger {
|
|
9414
|
+
display: flex; align-items: center; gap: clamp(5px, 0.36vw, 7px);
|
|
9415
|
+
padding: clamp(6px, 0.36vw, 8px) clamp(10px, 0.73vw, 14px);
|
|
9416
|
+
border: 1.5px solid var(--ps-border-color); border-radius: clamp(8px, 0.57vw, 10px);
|
|
9417
|
+
background: transparent; cursor: pointer; color: var(--ps-text-secondary);
|
|
9418
|
+
transition: all 0.25s ease; font-family: inherit; white-space: nowrap;
|
|
9419
|
+
min-height: clamp(28px, 2.2vw, 38px);
|
|
9487
9420
|
}
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
this.listeners.set(jobId, /* @__PURE__ */ new Set());
|
|
9491
|
-
}
|
|
9492
|
-
this.listeners.get(jobId).add(callback);
|
|
9493
|
-
if (!this.eventSource) {
|
|
9494
|
-
this.connect();
|
|
9495
|
-
}
|
|
9496
|
-
return () => {
|
|
9497
|
-
const jobListeners = this.listeners.get(jobId);
|
|
9498
|
-
if (jobListeners) {
|
|
9499
|
-
jobListeners.delete(callback);
|
|
9500
|
-
if (jobListeners.size === 0) {
|
|
9501
|
-
this.listeners.delete(jobId);
|
|
9502
|
-
}
|
|
9503
|
-
}
|
|
9504
|
-
if (this.listeners.size === 0) {
|
|
9505
|
-
this.disconnect();
|
|
9506
|
-
}
|
|
9507
|
-
};
|
|
9421
|
+
.ps-tryon-lang-trigger:hover, .ps-tryon-lang-trigger.ps-active {
|
|
9422
|
+
border-color: var(--ps-accent); color: var(--ps-accent); background: rgba(33,84,239,0.06);
|
|
9508
9423
|
}
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
callbacks.forEach((cb2) => cb2(update));
|
|
9513
|
-
}
|
|
9424
|
+
.ps-tryon-lang-trigger svg { stroke: currentColor; fill: none; flex-shrink: 0; width: clamp(13px, 0.8vw, 16px); height: clamp(13px, 0.8vw, 16px); }
|
|
9425
|
+
.ps-tryon-lang-current {
|
|
9426
|
+
font-size: clamp(12px, 0.68vw, 14px); font-weight: 500; letter-spacing: 0.01em;
|
|
9514
9427
|
}
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
clearTimeout(this.reconnectTimer);
|
|
9518
|
-
this.reconnectTimer = null;
|
|
9519
|
-
}
|
|
9520
|
-
if (this.eventSource) {
|
|
9521
|
-
this.eventSource.close();
|
|
9522
|
-
this.eventSource = null;
|
|
9523
|
-
}
|
|
9524
|
-
this.listeners.clear();
|
|
9525
|
-
this.reconnectAttempts = 0;
|
|
9428
|
+
.ps-tryon-lang-arrow {
|
|
9429
|
+
font-size: clamp(10px, 0.57vw, 12px); transition: transform 0.25s ease; display: inline-block;
|
|
9526
9430
|
}
|
|
9527
|
-
}
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
const canvas = document.createElement("canvas");
|
|
9540
|
-
let { width, height } = img;
|
|
9541
|
-
if (width > maxDim || height > maxDim) {
|
|
9542
|
-
if (width > height) {
|
|
9543
|
-
height = Math.round(height * maxDim / width);
|
|
9544
|
-
width = maxDim;
|
|
9545
|
-
} else {
|
|
9546
|
-
width = Math.round(width * maxDim / height);
|
|
9547
|
-
height = maxDim;
|
|
9548
|
-
}
|
|
9549
|
-
}
|
|
9550
|
-
canvas.width = width;
|
|
9551
|
-
canvas.height = height;
|
|
9552
|
-
const ctx = canvas.getContext("2d");
|
|
9553
|
-
if (!ctx) {
|
|
9554
|
-
reject(new Error("Canvas context not available"));
|
|
9555
|
-
return;
|
|
9556
|
-
}
|
|
9557
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
9558
|
-
const dataUrl = canvas.toDataURL("image/jpeg", quality);
|
|
9559
|
-
resolve(dataUrl);
|
|
9560
|
-
} catch (err) {
|
|
9561
|
-
reject(err);
|
|
9562
|
-
}
|
|
9563
|
-
};
|
|
9564
|
-
img.onerror = () => reject(new Error("Failed to load image"));
|
|
9565
|
-
img.src = reader.result;
|
|
9566
|
-
};
|
|
9567
|
-
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
9568
|
-
reader.readAsDataURL(file);
|
|
9569
|
-
});
|
|
9570
|
-
}
|
|
9571
|
-
function isValidImageFile(file) {
|
|
9572
|
-
const accepted = ["image/jpeg", "image/png", "image/webp", "image/avif"];
|
|
9573
|
-
return accepted.includes(file.type);
|
|
9574
|
-
}
|
|
9575
|
-
async function checkAgeBeforeUpload(imageOrBase64, apiUrl, apiKey) {
|
|
9576
|
-
try {
|
|
9577
|
-
const base64 = typeof imageOrBase64 === "string" ? imageOrBase64 : await compressImage(imageOrBase64, { maxDimension: 384, quality: 0.6 });
|
|
9578
|
-
const resp = await fetch(`${apiUrl}/api/v1/sizing/age-check`, {
|
|
9579
|
-
method: "POST",
|
|
9580
|
-
headers: {
|
|
9581
|
-
"Content-Type": "application/json",
|
|
9582
|
-
Authorization: `Bearer ${apiKey}`
|
|
9583
|
-
},
|
|
9584
|
-
body: JSON.stringify({ bodyImage: base64 })
|
|
9585
|
-
});
|
|
9586
|
-
if (!resp.ok) {
|
|
9587
|
-
console.warn(`[PS-SDK:AgeCheck] HTTP ${resp.status} — failing open`);
|
|
9588
|
-
return { isAdult: true, confidence: "low" };
|
|
9589
|
-
}
|
|
9590
|
-
const data = await resp.json();
|
|
9591
|
-
return {
|
|
9592
|
-
isAdult: data?.isAdult !== false,
|
|
9593
|
-
// anything other than explicit false → allow
|
|
9594
|
-
confidence: data?.confidence === "high" ? "high" : "low",
|
|
9595
|
-
reasoning: typeof data?.reasoning === "string" ? data.reasoning : void 0
|
|
9596
|
-
};
|
|
9597
|
-
} catch (err) {
|
|
9598
|
-
console.warn(`[PS-SDK:AgeCheck] error — failing open:`, err);
|
|
9599
|
-
return { isAdult: true, confidence: "low" };
|
|
9600
|
-
}
|
|
9601
|
-
}
|
|
9602
|
-
const DB_NAME = "primestyle-photos";
|
|
9603
|
-
const STORE = "photos";
|
|
9604
|
-
const VERSION = 1;
|
|
9605
|
-
let _dbPromise = null;
|
|
9606
|
-
function openDb() {
|
|
9607
|
-
if (_dbPromise) return _dbPromise;
|
|
9608
|
-
_dbPromise = new Promise((resolve) => {
|
|
9609
|
-
try {
|
|
9610
|
-
if (typeof indexedDB === "undefined") return resolve(null);
|
|
9611
|
-
const req = indexedDB.open(DB_NAME, VERSION);
|
|
9612
|
-
req.onupgradeneeded = () => {
|
|
9613
|
-
const db2 = req.result;
|
|
9614
|
-
if (!db2.objectStoreNames.contains(STORE)) {
|
|
9615
|
-
db2.createObjectStore(STORE);
|
|
9616
|
-
}
|
|
9617
|
-
};
|
|
9618
|
-
req.onsuccess = () => resolve(req.result);
|
|
9619
|
-
req.onerror = () => resolve(null);
|
|
9620
|
-
req.onblocked = () => resolve(null);
|
|
9621
|
-
} catch {
|
|
9622
|
-
resolve(null);
|
|
9623
|
-
}
|
|
9624
|
-
});
|
|
9625
|
-
return _dbPromise;
|
|
9626
|
-
}
|
|
9627
|
-
async function savePhoto(id2, blob) {
|
|
9628
|
-
const db2 = await openDb();
|
|
9629
|
-
if (!db2) return false;
|
|
9630
|
-
return new Promise((resolve) => {
|
|
9631
|
-
try {
|
|
9632
|
-
const tx = db2.transaction(STORE, "readwrite");
|
|
9633
|
-
tx.objectStore(STORE).put(blob, id2);
|
|
9634
|
-
tx.oncomplete = () => resolve(true);
|
|
9635
|
-
tx.onerror = () => resolve(false);
|
|
9636
|
-
tx.onabort = () => resolve(false);
|
|
9637
|
-
} catch {
|
|
9638
|
-
resolve(false);
|
|
9639
|
-
}
|
|
9640
|
-
});
|
|
9641
|
-
}
|
|
9642
|
-
async function getPhoto(id2) {
|
|
9643
|
-
const db2 = await openDb();
|
|
9644
|
-
if (!db2) return null;
|
|
9645
|
-
return new Promise((resolve) => {
|
|
9646
|
-
try {
|
|
9647
|
-
const tx = db2.transaction(STORE, "readonly");
|
|
9648
|
-
const req = tx.objectStore(STORE).get(id2);
|
|
9649
|
-
req.onsuccess = () => resolve(req.result instanceof Blob ? req.result : null);
|
|
9650
|
-
req.onerror = () => resolve(null);
|
|
9651
|
-
} catch {
|
|
9652
|
-
resolve(null);
|
|
9653
|
-
}
|
|
9654
|
-
});
|
|
9655
|
-
}
|
|
9656
|
-
async function pruneToIds(keepIds) {
|
|
9657
|
-
const db2 = await openDb();
|
|
9658
|
-
if (!db2) return;
|
|
9659
|
-
return new Promise((resolve) => {
|
|
9660
|
-
try {
|
|
9661
|
-
const tx = db2.transaction(STORE, "readwrite");
|
|
9662
|
-
const store = tx.objectStore(STORE);
|
|
9663
|
-
const cursorReq = store.openKeyCursor();
|
|
9664
|
-
cursorReq.onsuccess = () => {
|
|
9665
|
-
const cursor = cursorReq.result;
|
|
9666
|
-
if (!cursor) return;
|
|
9667
|
-
const key = String(cursor.key);
|
|
9668
|
-
if (!keepIds.has(key)) cursor.delete();
|
|
9669
|
-
cursor.continue();
|
|
9670
|
-
};
|
|
9671
|
-
tx.oncomplete = () => resolve();
|
|
9672
|
-
tx.onerror = () => resolve();
|
|
9673
|
-
} catch {
|
|
9674
|
-
resolve();
|
|
9675
|
-
}
|
|
9676
|
-
});
|
|
9677
|
-
}
|
|
9678
|
-
const LEFT_SHOULDER = 11;
|
|
9679
|
-
const RIGHT_SHOULDER = 12;
|
|
9680
|
-
const LEFT_ELBOW = 13;
|
|
9681
|
-
const RIGHT_ELBOW = 14;
|
|
9682
|
-
const LEFT_WRIST = 15;
|
|
9683
|
-
const RIGHT_WRIST = 16;
|
|
9684
|
-
const LEFT_HIP = 23;
|
|
9685
|
-
const RIGHT_HIP = 24;
|
|
9686
|
-
const LEFT_KNEE = 25;
|
|
9687
|
-
const RIGHT_KNEE = 26;
|
|
9688
|
-
const LEFT_ANKLE = 27;
|
|
9689
|
-
const RIGHT_ANKLE = 28;
|
|
9690
|
-
const NOSE = 0;
|
|
9691
|
-
let poseLandmarker = null;
|
|
9692
|
-
let loadingPromise$1 = null;
|
|
9693
|
-
async function loadMediaPipe() {
|
|
9694
|
-
if (poseLandmarker) return;
|
|
9695
|
-
if (loadingPromise$1) return loadingPromise$1;
|
|
9696
|
-
loadingPromise$1 = (async () => {
|
|
9697
|
-
const vision = await import(
|
|
9698
|
-
/* webpackIgnore: true */
|
|
9699
|
-
// @ts-ignore dynamic CDN import
|
|
9700
|
-
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/vision_bundle.mjs"
|
|
9701
|
-
);
|
|
9702
|
-
const { FilesetResolver, PoseLandmarker } = vision;
|
|
9703
|
-
const filesetResolver = await FilesetResolver.forVisionTasks(
|
|
9704
|
-
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/wasm"
|
|
9705
|
-
);
|
|
9706
|
-
poseLandmarker = await PoseLandmarker.createFromOptions(filesetResolver, {
|
|
9707
|
-
baseOptions: {
|
|
9708
|
-
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task",
|
|
9709
|
-
delegate: "GPU"
|
|
9710
|
-
},
|
|
9711
|
-
runningMode: "IMAGE",
|
|
9712
|
-
numPoses: 1
|
|
9713
|
-
});
|
|
9714
|
-
})();
|
|
9715
|
-
return loadingPromise$1;
|
|
9716
|
-
}
|
|
9717
|
-
async function detectMeasurementLines(imageSrc) {
|
|
9718
|
-
try {
|
|
9719
|
-
await loadMediaPipe();
|
|
9720
|
-
const img = await loadImage$1(imageSrc);
|
|
9721
|
-
const result = poseLandmarker.detect(img);
|
|
9722
|
-
if (!result?.landmarks?.length || result.landmarks[0].length < 25) {
|
|
9723
|
-
return null;
|
|
9724
|
-
}
|
|
9725
|
-
const landmarks = result.landmarks[0];
|
|
9726
|
-
const lShoulder = landmarks[LEFT_SHOULDER];
|
|
9727
|
-
const rShoulder = landmarks[RIGHT_SHOULDER];
|
|
9728
|
-
const lHip = landmarks[LEFT_HIP];
|
|
9729
|
-
const rHip = landmarks[RIGHT_HIP];
|
|
9730
|
-
const shoulderY = (lShoulder.y + rShoulder.y) / 2;
|
|
9731
|
-
const hipY = (lHip.y + rHip.y) / 2;
|
|
9732
|
-
const torsoLen = hipY - shoulderY;
|
|
9733
|
-
const chestY = shoulderY + torsoLen * 0.28;
|
|
9734
|
-
const shoulderWidth = Math.abs(rShoulder.x - lShoulder.x);
|
|
9735
|
-
const chestX1 = Math.min(lShoulder.x, rShoulder.x) - shoulderWidth * 0.05;
|
|
9736
|
-
const chestX2 = Math.max(lShoulder.x, rShoulder.x) + shoulderWidth * 0.05;
|
|
9737
|
-
const hipWidth = Math.abs(rHip.x - lHip.x);
|
|
9738
|
-
const hipX1 = Math.min(lHip.x, rHip.x) - hipWidth * 0.15;
|
|
9739
|
-
const hipX2 = Math.max(lHip.x, rHip.x) + hipWidth * 0.15;
|
|
9740
|
-
const waistY = chestY + (hipY - chestY) * 0.55;
|
|
9741
|
-
const waistWidth = shoulderWidth * 0.85;
|
|
9742
|
-
const waistCenterX = (lShoulder.x + rShoulder.x + lHip.x + rHip.x) / 4;
|
|
9743
|
-
const waistX1 = waistCenterX - waistWidth / 2;
|
|
9744
|
-
const waistX2 = waistCenterX + waistWidth / 2;
|
|
9745
|
-
return {
|
|
9746
|
-
chest: { y: chestY, x1: chestX1, x2: chestX2 },
|
|
9747
|
-
waist: { y: waistY, x1: waistX1, x2: waistX2 },
|
|
9748
|
-
hips: { y: hipY, x1: hipX1, x2: hipX2 }
|
|
9749
|
-
};
|
|
9750
|
-
} catch (err) {
|
|
9751
|
-
console.error("[PS-SDK] Pose detection failed:", err);
|
|
9752
|
-
return null;
|
|
9753
|
-
}
|
|
9754
|
-
}
|
|
9755
|
-
function loadImage$1(src) {
|
|
9756
|
-
return new Promise((resolve, reject) => {
|
|
9757
|
-
const img = new Image();
|
|
9758
|
-
img.crossOrigin = "anonymous";
|
|
9759
|
-
img.onload = () => resolve(img);
|
|
9760
|
-
img.onerror = () => {
|
|
9761
|
-
const img2 = new Image();
|
|
9762
|
-
img2.onload = () => resolve(img2);
|
|
9763
|
-
img2.onerror = () => reject(new Error("Failed to load image"));
|
|
9764
|
-
img2.src = src;
|
|
9765
|
-
};
|
|
9766
|
-
img.src = src;
|
|
9767
|
-
});
|
|
9768
|
-
}
|
|
9769
|
-
async function downsampleIfNeeded(img, maxLongEdge = 1024) {
|
|
9770
|
-
const w2 = img.naturalWidth || img.width;
|
|
9771
|
-
const h = img.naturalHeight || img.height;
|
|
9772
|
-
const longEdge = Math.max(w2, h);
|
|
9773
|
-
if (longEdge <= maxLongEdge) return img;
|
|
9774
|
-
const scale = maxLongEdge / longEdge;
|
|
9775
|
-
const newW = Math.round(w2 * scale);
|
|
9776
|
-
const newH = Math.round(h * scale);
|
|
9777
|
-
const canvas = document.createElement("canvas");
|
|
9778
|
-
canvas.width = newW;
|
|
9779
|
-
canvas.height = newH;
|
|
9780
|
-
const ctx = canvas.getContext("2d");
|
|
9781
|
-
if (!ctx) return img;
|
|
9782
|
-
ctx.imageSmoothingQuality = "high";
|
|
9783
|
-
ctx.drawImage(img, 0, 0, newW, newH);
|
|
9784
|
-
console.log(`[PS-SDK:Downsample] ${w2}×${h} → ${newW}×${newH} (max long edge ${maxLongEdge}px)`);
|
|
9785
|
-
return canvas;
|
|
9786
|
-
}
|
|
9787
|
-
async function detectBodyLandmarks(imageSrc) {
|
|
9788
|
-
try {
|
|
9789
|
-
await loadMediaPipe();
|
|
9790
|
-
let img;
|
|
9791
|
-
if (typeof imageSrc === "string") {
|
|
9792
|
-
img = await loadImage$1(imageSrc);
|
|
9793
|
-
} else {
|
|
9794
|
-
img = imageSrc;
|
|
9795
|
-
}
|
|
9796
|
-
const inputForDetect = await downsampleIfNeeded(img, 1024);
|
|
9797
|
-
const workingW = inputForDetect.width || img.naturalWidth || img.width;
|
|
9798
|
-
const workingH = inputForDetect.height || img.naturalHeight || img.height;
|
|
9799
|
-
const result = poseLandmarker.detect(inputForDetect);
|
|
9800
|
-
if (!result?.landmarks?.length || result.landmarks[0].length < 29) {
|
|
9801
|
-
console.warn("[PS-SDK:MediaPipe] detection returned no usable landmarks", {
|
|
9802
|
-
gotLandmarks: !!result?.landmarks?.length,
|
|
9803
|
-
jointCount: result?.landmarks?.[0]?.length ?? 0
|
|
9804
|
-
});
|
|
9805
|
-
return null;
|
|
9806
|
-
}
|
|
9807
|
-
const lm = result.landmarks[0];
|
|
9808
|
-
const wlm = result.worldLandmarks?.[0] ?? null;
|
|
9809
|
-
const pickPoint = (idx, useWorld = false) => {
|
|
9810
|
-
const src = useWorld && wlm ? wlm[idx] : lm[idx];
|
|
9811
|
-
const out2 = { x: src.x, y: src.y };
|
|
9812
|
-
if (typeof src.z === "number") out2.z = src.z;
|
|
9813
|
-
if (typeof src.visibility === "number") out2.visibility = src.visibility;
|
|
9814
|
-
return out2;
|
|
9815
|
-
};
|
|
9816
|
-
const out = {
|
|
9817
|
-
leftShoulder: pickPoint(LEFT_SHOULDER),
|
|
9818
|
-
rightShoulder: pickPoint(RIGHT_SHOULDER),
|
|
9819
|
-
leftHip: pickPoint(LEFT_HIP),
|
|
9820
|
-
rightHip: pickPoint(RIGHT_HIP),
|
|
9821
|
-
leftElbow: pickPoint(LEFT_ELBOW),
|
|
9822
|
-
rightElbow: pickPoint(RIGHT_ELBOW),
|
|
9823
|
-
leftWrist: pickPoint(LEFT_WRIST),
|
|
9824
|
-
rightWrist: pickPoint(RIGHT_WRIST),
|
|
9825
|
-
leftKnee: pickPoint(LEFT_KNEE),
|
|
9826
|
-
rightKnee: pickPoint(RIGHT_KNEE),
|
|
9827
|
-
leftAnkle: pickPoint(LEFT_ANKLE),
|
|
9828
|
-
rightAnkle: pickPoint(RIGHT_ANKLE),
|
|
9829
|
-
nose: pickPoint(NOSE),
|
|
9830
|
-
imageWidth: workingW,
|
|
9831
|
-
imageHeight: workingH
|
|
9832
|
-
};
|
|
9833
|
-
const isJointPoint = (v2) => !!v2 && typeof v2 === "object" && typeof v2.x === "number" && typeof v2.y === "number";
|
|
9834
|
-
const jointCount = Object.values(out).filter(isJointPoint).length;
|
|
9835
|
-
const hasZ = lm.some((p2) => typeof p2.z === "number");
|
|
9836
|
-
const hasWorld = !!wlm;
|
|
9837
|
-
console.groupCollapsed(`[PS-SDK:MediaPipe] ✓ ${jointCount} joints detected @ ${out.imageWidth}×${out.imageHeight}px (z=${hasZ ? "yes" : "no"}, worldLandmarks=${hasWorld ? "yes" : "no"})`);
|
|
9838
|
-
for (const [name, v2] of Object.entries(out)) {
|
|
9839
|
-
if (!isJointPoint(v2)) continue;
|
|
9840
|
-
const p2 = v2;
|
|
9841
|
-
const z2 = typeof p2.z === "number" ? `, z=${p2.z.toFixed(3)}` : "";
|
|
9842
|
-
const vis = typeof p2.visibility === "number" ? `, vis=${p2.visibility.toFixed(2)}` : "";
|
|
9843
|
-
console.log(` ${name.padEnd(15)} (${p2.x.toFixed(3)}, ${p2.y.toFixed(3)}${z2}${vis})`);
|
|
9844
|
-
}
|
|
9845
|
-
console.log(` imageWidth ${out.imageWidth}`);
|
|
9846
|
-
console.log(` imageHeight ${out.imageHeight}`);
|
|
9847
|
-
console.groupEnd();
|
|
9848
|
-
return out;
|
|
9849
|
-
} catch (err) {
|
|
9850
|
-
console.error("[PS-SDK:MediaPipe] Body landmark detection failed:", err);
|
|
9851
|
-
return null;
|
|
9852
|
-
}
|
|
9853
|
-
}
|
|
9854
|
-
const IDX = {
|
|
9855
|
-
noseTip: 1,
|
|
9856
|
-
noseBridge: 168,
|
|
9857
|
-
leftInnerEye: 133,
|
|
9858
|
-
rightInnerEye: 362,
|
|
9859
|
-
leftOuterEye: 33,
|
|
9860
|
-
rightOuterEye: 263,
|
|
9861
|
-
// Iris landmarks (478-point model with iris refinement)
|
|
9862
|
-
leftIrisCenter: 468,
|
|
9863
|
-
leftIrisRing: [469, 470, 471, 472],
|
|
9864
|
-
rightIrisCenter: 473,
|
|
9865
|
-
rightIrisRing: [474, 475, 476, 477],
|
|
9866
|
-
// Tragus (ear attach point) — best approximations in the face mesh
|
|
9867
|
-
leftTragus: 234,
|
|
9868
|
-
rightTragus: 454,
|
|
9869
|
-
forehead: 10,
|
|
9870
|
-
chin: 152,
|
|
9871
|
-
leftMouth: 61,
|
|
9872
|
-
rightMouth: 291
|
|
9873
|
-
};
|
|
9874
|
-
const IRIS_DIAMETER_MM = 11.7;
|
|
9875
|
-
let faceLandmarker = null;
|
|
9876
|
-
let loadingPromise = null;
|
|
9877
|
-
async function loadFaceLandmarker() {
|
|
9878
|
-
if (faceLandmarker) {
|
|
9879
|
-
console.log("[PS-SDK:FaceDetect] loadFaceLandmarker — already loaded, reusing");
|
|
9880
|
-
return;
|
|
9881
|
-
}
|
|
9882
|
-
if (loadingPromise) {
|
|
9883
|
-
console.log("[PS-SDK:FaceDetect] loadFaceLandmarker — awaiting in-flight load");
|
|
9884
|
-
return loadingPromise;
|
|
9885
|
-
}
|
|
9886
|
-
console.log("[PS-SDK:FaceDetect] loadFaceLandmarker — starting fresh init");
|
|
9887
|
-
loadingPromise = (async () => {
|
|
9888
|
-
try {
|
|
9889
|
-
console.log("[PS-SDK:FaceDetect] importing vision_bundle.mjs from CDN…");
|
|
9890
|
-
const t0 = performance.now();
|
|
9891
|
-
const vision = await import(
|
|
9892
|
-
/* webpackIgnore: true */
|
|
9893
|
-
// @ts-ignore dynamic CDN import
|
|
9894
|
-
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/vision_bundle.mjs"
|
|
9895
|
-
);
|
|
9896
|
-
console.log(`[PS-SDK:FaceDetect] vision bundle imported in ${Math.round(performance.now() - t0)}ms`);
|
|
9897
|
-
const { FilesetResolver, FaceLandmarker } = vision;
|
|
9898
|
-
if (!FilesetResolver || !FaceLandmarker) {
|
|
9899
|
-
console.error("[PS-SDK:FaceDetect] vision bundle missing expected exports", { keys: Object.keys(vision) });
|
|
9900
|
-
throw new Error("MediaPipe vision bundle returned no FilesetResolver/FaceLandmarker");
|
|
9901
|
-
}
|
|
9902
|
-
console.log("[PS-SDK:FaceDetect] resolving WASM filesets…");
|
|
9903
|
-
const t1 = performance.now();
|
|
9904
|
-
const filesetResolver = await FilesetResolver.forVisionTasks(
|
|
9905
|
-
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/wasm"
|
|
9906
|
-
);
|
|
9907
|
-
console.log(`[PS-SDK:FaceDetect] WASM resolver ready in ${Math.round(performance.now() - t1)}ms`);
|
|
9908
|
-
console.log("[PS-SDK:FaceDetect] creating FaceLandmarker (GPU delegate, model=latest)…");
|
|
9909
|
-
const t2 = performance.now();
|
|
9910
|
-
faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver, {
|
|
9911
|
-
baseOptions: {
|
|
9912
|
-
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
|
|
9913
|
-
delegate: "GPU"
|
|
9914
|
-
},
|
|
9915
|
-
runningMode: "IMAGE",
|
|
9916
|
-
numFaces: 1,
|
|
9917
|
-
outputFaceBlendshapes: false,
|
|
9918
|
-
outputFacialTransformationMatrixes: false
|
|
9919
|
-
});
|
|
9920
|
-
console.log(`[PS-SDK:FaceDetect] FaceLandmarker ready in ${Math.round(performance.now() - t2)}ms`);
|
|
9921
|
-
} catch (err) {
|
|
9922
|
-
console.error("[PS-SDK:FaceDetect] loadFaceLandmarker FAILED:", err);
|
|
9923
|
-
loadingPromise = null;
|
|
9924
|
-
throw err;
|
|
9925
|
-
}
|
|
9926
|
-
})();
|
|
9927
|
-
return loadingPromise;
|
|
9928
|
-
}
|
|
9929
|
-
function loadImage(src) {
|
|
9930
|
-
return new Promise((resolve, reject) => {
|
|
9931
|
-
const img = new Image();
|
|
9932
|
-
img.crossOrigin = "anonymous";
|
|
9933
|
-
img.onload = () => {
|
|
9934
|
-
console.log("[PS-SDK:FaceDetect] loadImage — primary (crossOrigin) succeeded", { w: img.naturalWidth, h: img.naturalHeight });
|
|
9935
|
-
resolve(img);
|
|
9936
|
-
};
|
|
9937
|
-
img.onerror = () => {
|
|
9938
|
-
console.warn("[PS-SDK:FaceDetect] loadImage — primary (crossOrigin) failed, retrying without crossOrigin");
|
|
9939
|
-
const img2 = new Image();
|
|
9940
|
-
img2.onload = () => {
|
|
9941
|
-
console.log("[PS-SDK:FaceDetect] loadImage — fallback succeeded", { w: img2.naturalWidth, h: img2.naturalHeight });
|
|
9942
|
-
resolve(img2);
|
|
9943
|
-
};
|
|
9944
|
-
img2.onerror = () => {
|
|
9945
|
-
console.error("[PS-SDK:FaceDetect] loadImage — both attempts failed for src:", src.slice(0, 120));
|
|
9946
|
-
reject(new Error("Failed to load image"));
|
|
9947
|
-
};
|
|
9948
|
-
img2.src = src;
|
|
9949
|
-
};
|
|
9950
|
-
img.src = src;
|
|
9951
|
-
});
|
|
9952
|
-
}
|
|
9953
|
-
function irisDiameterPx(ring, imageWidth, imageHeight) {
|
|
9954
|
-
if (ring.length < 4) return 0;
|
|
9955
|
-
const toPx = (p4) => ({ x: p4.x * imageWidth, y: p4.y * imageHeight });
|
|
9956
|
-
const [p0, p1, p2, p3] = ring.map(toPx);
|
|
9957
|
-
const d1 = Math.hypot(p0.x - p2.x, p0.y - p2.y);
|
|
9958
|
-
const d2 = Math.hypot(p1.x - p3.x, p1.y - p3.y);
|
|
9959
|
-
return (d1 + d2) / 2;
|
|
9960
|
-
}
|
|
9961
|
-
function extractLandmarks(raw) {
|
|
9962
|
-
if (raw.length < 478) {
|
|
9963
|
-
console.warn(`[PS-SDK:FaceDetect] extractLandmarks REJECTED — need 478 landmarks (iris refinement), got ${raw.length}. The model is missing iris data — probably the CDN "latest" tag moved to a non-iris variant.`);
|
|
9964
|
-
return null;
|
|
9965
|
-
}
|
|
9966
|
-
const at = (i) => ({ x: raw[i].x, y: raw[i].y, z: raw[i].z });
|
|
9967
|
-
return {
|
|
9968
|
-
noseTip: at(IDX.noseTip),
|
|
9969
|
-
noseBridge: at(IDX.noseBridge),
|
|
9970
|
-
leftInnerEye: at(IDX.leftInnerEye),
|
|
9971
|
-
rightInnerEye: at(IDX.rightInnerEye),
|
|
9972
|
-
leftOuterEye: at(IDX.leftOuterEye),
|
|
9973
|
-
rightOuterEye: at(IDX.rightOuterEye),
|
|
9974
|
-
leftIrisCenter: at(IDX.leftIrisCenter),
|
|
9975
|
-
rightIrisCenter: at(IDX.rightIrisCenter),
|
|
9976
|
-
leftIrisRing: IDX.leftIrisRing.map(at),
|
|
9977
|
-
rightIrisRing: IDX.rightIrisRing.map(at),
|
|
9978
|
-
leftTragus: at(IDX.leftTragus),
|
|
9979
|
-
rightTragus: at(IDX.rightTragus),
|
|
9980
|
-
forehead: at(IDX.forehead),
|
|
9981
|
-
chin: at(IDX.chin),
|
|
9982
|
-
leftMouth: at(IDX.leftMouth),
|
|
9983
|
-
rightMouth: at(IDX.rightMouth)
|
|
9984
|
-
};
|
|
9985
|
-
}
|
|
9986
|
-
function computeMeasurements(lm, imageWidth, imageHeight) {
|
|
9987
|
-
const leftPx = irisDiameterPx(lm.leftIrisRing, imageWidth, imageHeight);
|
|
9988
|
-
const rightPx = irisDiameterPx(lm.rightIrisRing, imageWidth, imageHeight);
|
|
9989
|
-
const irisPx = (leftPx + rightPx) / 2;
|
|
9990
|
-
let irisConfidence = 1;
|
|
9991
|
-
if (irisPx < 8) irisConfidence = 0.3;
|
|
9992
|
-
else if (Math.abs(leftPx - rightPx) / irisPx > 0.3) irisConfidence = 0.5;
|
|
9993
|
-
else if (Math.abs(leftPx - rightPx) / irisPx > 0.15) irisConfidence = 0.8;
|
|
9994
|
-
const pxToMm = irisPx > 0 ? IRIS_DIAMETER_MM / irisPx : 0;
|
|
9995
|
-
const mmBetween = (a2, b2) => {
|
|
9996
|
-
const pxDist = Math.hypot(
|
|
9997
|
-
(a2.x - b2.x) * imageWidth,
|
|
9998
|
-
(a2.y - b2.y) * imageHeight
|
|
9999
|
-
);
|
|
10000
|
-
return pxDist * pxToMm;
|
|
10001
|
-
};
|
|
10002
|
-
const pd2 = mmBetween(lm.leftIrisCenter, lm.rightIrisCenter);
|
|
10003
|
-
const innerEyeGap = mmBetween(lm.leftInnerEye, lm.rightInnerEye);
|
|
10004
|
-
const bridgeWidth = innerEyeGap / 1.6;
|
|
10005
|
-
const faceWidth = mmBetween(lm.leftTragus, lm.rightTragus);
|
|
10006
|
-
const templeLengthLeft = mmBetween(lm.leftTragus, lm.leftOuterEye);
|
|
10007
|
-
const templeLengthRight = mmBetween(lm.rightTragus, lm.rightOuterEye);
|
|
10008
|
-
const templeLength = (templeLengthLeft + templeLengthRight) / 2;
|
|
10009
|
-
const headWidth = faceWidth * 1.07;
|
|
10010
|
-
const zDepthNorm = Math.abs((lm.forehead.z ?? 0) - (lm.chin.z ?? 0));
|
|
10011
|
-
const rawHeadDepthMm = zDepthNorm * imageWidth * pxToMm;
|
|
10012
|
-
const headDepth = Math.max(170, Math.min(210, rawHeadDepthMm || 190));
|
|
10013
|
-
const a = headWidth / 2;
|
|
10014
|
-
const b = headDepth / 2;
|
|
10015
|
-
const headCircumference = Math.PI * Math.sqrt(2 * (a * a + b * b));
|
|
10016
|
-
return {
|
|
10017
|
-
measurements: {
|
|
10018
|
-
irisDiameter: IRIS_DIAMETER_MM,
|
|
10019
|
-
pd: round1(pd2),
|
|
10020
|
-
bridgeWidth: round1(bridgeWidth),
|
|
10021
|
-
faceWidth: round1(faceWidth),
|
|
10022
|
-
templeLengthLeft: round1(templeLengthLeft),
|
|
10023
|
-
templeLengthRight: round1(templeLengthRight),
|
|
10024
|
-
templeLength: round1(templeLength),
|
|
10025
|
-
headWidth: round1(headWidth),
|
|
10026
|
-
headDepth: round1(headDepth),
|
|
10027
|
-
headCircumference: round1(headCircumference)
|
|
10028
|
-
},
|
|
10029
|
-
irisConfidence
|
|
10030
|
-
};
|
|
10031
|
-
}
|
|
10032
|
-
function round1(n2) {
|
|
10033
|
-
return Math.round(n2 * 10) / 10;
|
|
10034
|
-
}
|
|
10035
|
-
async function detectFaceMeasurements(imageSrc) {
|
|
10036
|
-
console.log("[PS-SDK:FaceDetect] detectFaceMeasurements called", {
|
|
10037
|
-
srcType: typeof imageSrc === "string" ? imageSrc.startsWith("blob:") ? "blob-url" : imageSrc.startsWith("data:") ? "data-url" : "other" : "HTMLImageElement",
|
|
10038
|
-
srcLength: typeof imageSrc === "string" ? imageSrc.length : "N/A"
|
|
10039
|
-
});
|
|
10040
|
-
const tStart = performance.now();
|
|
10041
|
-
try {
|
|
10042
|
-
console.log("[PS-SDK:FaceDetect] step 1/4 — loading FaceLandmarker");
|
|
10043
|
-
await loadFaceLandmarker();
|
|
10044
|
-
console.log("[PS-SDK:FaceDetect] step 2/4 — loading image");
|
|
10045
|
-
const img = typeof imageSrc === "string" ? await loadImage(imageSrc) : imageSrc;
|
|
10046
|
-
const imageWidth = img.naturalWidth || img.width;
|
|
10047
|
-
const imageHeight = img.naturalHeight || img.height;
|
|
10048
|
-
console.log("[PS-SDK:FaceDetect] step 3/4 — running faceLandmarker.detect()", { imageWidth, imageHeight });
|
|
10049
|
-
const tDetect = performance.now();
|
|
10050
|
-
const result = faceLandmarker.detect(img);
|
|
10051
|
-
const detectMs = Math.round(performance.now() - tDetect);
|
|
10052
|
-
const faceCount = result?.faceLandmarks?.length ?? 0;
|
|
10053
|
-
console.log(`[PS-SDK:FaceDetect] detect() returned in ${detectMs}ms — faces: ${faceCount}`);
|
|
10054
|
-
if (!faceCount) {
|
|
10055
|
-
console.warn("[PS-SDK:FaceDetect] NO FACES detected — returning null");
|
|
10056
|
-
return null;
|
|
10057
|
-
}
|
|
10058
|
-
const raw = result.faceLandmarks[0];
|
|
10059
|
-
console.log(`[PS-SDK:FaceDetect] first face has ${raw.length} landmarks`);
|
|
10060
|
-
const landmarks = extractLandmarks(raw);
|
|
10061
|
-
if (!landmarks) {
|
|
10062
|
-
return null;
|
|
10063
|
-
}
|
|
10064
|
-
console.log("[PS-SDK:FaceDetect] step 4/4 — computing measurements");
|
|
10065
|
-
const { measurements, irisConfidence } = computeMeasurements(landmarks, imageWidth, imageHeight);
|
|
10066
|
-
console.log(`[PS-SDK:FaceDetect] ✓ DONE in ${Math.round(performance.now() - tStart)}ms`, { irisConfidence, measurements });
|
|
10067
|
-
return { landmarks, measurementsMm: measurements, irisConfidence, imageWidth, imageHeight };
|
|
10068
|
-
} catch (err) {
|
|
10069
|
-
console.error("[PS-SDK:FaceDetect] detectFaceMeasurements FAILED:", err);
|
|
10070
|
-
return null;
|
|
10071
|
-
}
|
|
10072
|
-
}
|
|
10073
|
-
function parseRange(s) {
|
|
10074
|
-
const ns = s.replace(/[^\d.\-–]/g, " ").trim().split(/[\s\-–]+/).filter(Boolean).map(Number).filter((n2) => !isNaN(n2));
|
|
10075
|
-
return ns.length ? { min: Math.min(...ns), max: Math.max(...ns) } : { min: 0, max: 0 };
|
|
10076
|
-
}
|
|
10077
|
-
function parseNum(s) {
|
|
10078
|
-
const n2 = parseFloat(s.replace(/[^\d.]/g, ""));
|
|
10079
|
-
return isNaN(n2) ? 0 : n2;
|
|
10080
|
-
}
|
|
10081
|
-
function computeFit(userValue, chartRange, unit) {
|
|
10082
|
-
const { min: rMin, max: rMax } = parseRange(chartRange);
|
|
10083
|
-
if (rMin === 0 && rMax === 0) return "good";
|
|
10084
|
-
const range = rMax - rMin;
|
|
10085
|
-
const threshold = range > 0 ? range * 0.5 : rMin * 0.05 || 3;
|
|
10086
|
-
const perfectTol = unit === "cm" ? 2.54 : unit === "mm" ? 25.4 : 1;
|
|
10087
|
-
if (userValue > rMin - perfectTol && userValue < rMax + perfectTol) return "good";
|
|
10088
|
-
if (userValue < rMin) {
|
|
10089
|
-
const diff2 = rMin - userValue;
|
|
10090
|
-
if (diff2 > threshold * 2) return "too-loose";
|
|
10091
|
-
if (diff2 > threshold) return "loose";
|
|
10092
|
-
return "a-bit-loose";
|
|
10093
|
-
}
|
|
10094
|
-
const diff = userValue - rMax;
|
|
10095
|
-
if (diff > threshold * 2) return "too-tight";
|
|
10096
|
-
if (diff > threshold) return "tight";
|
|
10097
|
-
return "a-bit-tight";
|
|
10098
|
-
}
|
|
10099
|
-
const AREA_TO_POSE_KEY = {
|
|
10100
|
-
chest: "chest",
|
|
10101
|
-
bust: "chest",
|
|
10102
|
-
waist: "waist",
|
|
10103
|
-
hips: "hips",
|
|
10104
|
-
hip: "hips"
|
|
10105
|
-
};
|
|
10106
|
-
const SKIP_AREAS_FOR_FIT = /* @__PURE__ */ new Set([
|
|
10107
|
-
"height",
|
|
10108
|
-
"altezza",
|
|
10109
|
-
"estatura"
|
|
10110
|
-
]);
|
|
10111
|
-
function buildFitInfo(matchDetails, poseLines, unit) {
|
|
10112
|
-
return matchDetails.filter((m2) => !SKIP_AREAS_FOR_FIT.has(m2.measurement.toLowerCase().replace(/\s*\(.*?\)\s*/g, "").trim())).map((m2) => {
|
|
10113
|
-
const userNum = parseNum(m2.userValue);
|
|
10114
|
-
const fit = computeFit(userNum, m2.chartRange, unit);
|
|
10115
|
-
const info = {
|
|
10116
|
-
area: m2.measurement,
|
|
10117
|
-
section: m2.section || void 0,
|
|
10118
|
-
fit,
|
|
10119
|
-
userValue: userNum || void 0,
|
|
10120
|
-
garmentRange: m2.chartRange || void 0
|
|
10121
|
-
};
|
|
10122
|
-
if (poseLines) {
|
|
10123
|
-
const poseKey = AREA_TO_POSE_KEY[m2.measurement.toLowerCase()];
|
|
10124
|
-
if (poseKey && poseLines[poseKey]) {
|
|
10125
|
-
const line = poseLines[poseKey];
|
|
10126
|
-
info.y = Math.round(line.y * 1e3) / 1e3;
|
|
10127
|
-
info.x1 = Math.round(line.x1 * 1e3) / 1e3;
|
|
10128
|
-
info.x2 = Math.round(line.x2 * 1e3) / 1e3;
|
|
10129
|
-
}
|
|
10130
|
-
}
|
|
10131
|
-
return info;
|
|
10132
|
-
});
|
|
10133
|
-
}
|
|
10134
|
-
function buildSilhouetteContext(sizingResult, sizeGuide, selectedSizeOverride) {
|
|
10135
|
-
if (!sizingResult && !sizeGuide) return void 0;
|
|
10136
|
-
const out = {};
|
|
10137
|
-
if (selectedSizeOverride) out.recommendedSize = selectedSizeOverride;
|
|
10138
|
-
else if (sizingResult?.recommendedSize) out.recommendedSize = sizingResult.recommendedSize;
|
|
10139
|
-
const seen = /* @__PURE__ */ new Set();
|
|
10140
|
-
const userLines = [];
|
|
10141
|
-
const push = (md2) => {
|
|
10142
|
-
if (!md2) return;
|
|
10143
|
-
for (const m2 of md2) {
|
|
10144
|
-
const k2 = m2.measurement.toLowerCase();
|
|
10145
|
-
if (seen.has(k2)) continue;
|
|
10146
|
-
seen.add(k2);
|
|
10147
|
-
if (m2.userValue) userLines.push(`${m2.measurement} ${m2.userValue}`);
|
|
10148
|
-
}
|
|
10149
|
-
};
|
|
10150
|
-
push(sizingResult?.matchDetails);
|
|
10151
|
-
if (sizingResult?.sections) {
|
|
10152
|
-
for (const sec of Object.values(sizingResult.sections)) push(sec.matchDetails);
|
|
10153
|
-
}
|
|
10154
|
-
if (userLines.length) out.userMeasurementsText = userLines.join(", ");
|
|
10155
|
-
if (sizeGuide?.headers?.length && sizeGuide.rows?.length) {
|
|
10156
|
-
const hdrs = sizeGuide.headers;
|
|
10157
|
-
const rowStrings = sizeGuide.rows.slice(0, 20).map(
|
|
10158
|
-
(row) => hdrs.map((h, i) => `${h} ${row[i] ?? "—"}`).join(", ")
|
|
10159
|
-
);
|
|
10160
|
-
out.sizeChartSummary = rowStrings.join(" | ");
|
|
10161
|
-
}
|
|
10162
|
-
return Object.keys(out).length ? out : void 0;
|
|
10163
|
-
}
|
|
10164
|
-
const FALLBACK_FIELDS_FEMALE = [
|
|
10165
|
-
{ key: "bust", label: "Bust", required: true, unit: "cm", placeholder: "e.g. 88", category: "body" },
|
|
10166
|
-
{ key: "waist", label: "Waist", required: true, unit: "cm", placeholder: "e.g. 70", category: "body" },
|
|
10167
|
-
{ key: "hips", label: "Hips", required: true, unit: "cm", placeholder: "e.g. 96", category: "body" },
|
|
10168
|
-
{ key: "shoulderWidth", label: "Shoulders", required: false, unit: "cm", placeholder: "e.g. 39", category: "body" },
|
|
10169
|
-
{ key: "inseam", label: "Inseam", required: false, unit: "cm", placeholder: "e.g. 76", category: "body" },
|
|
10170
|
-
{ key: "footLengthCm", label: "Foot length", required: false, unit: "cm", placeholder: "e.g. 24", category: "shoe" }
|
|
10171
|
-
];
|
|
10172
|
-
const FALLBACK_FIELDS_MALE = [
|
|
10173
|
-
{ key: "chest", label: "Chest", required: true, unit: "cm", placeholder: "e.g. 104", category: "body" },
|
|
10174
|
-
{ key: "waist", label: "Waist", required: true, unit: "cm", placeholder: "e.g. 84", category: "body" },
|
|
10175
|
-
{ key: "shoulderWidth", label: "Shoulders", required: false, unit: "cm", placeholder: "e.g. 46", category: "body" },
|
|
10176
|
-
{ key: "sleeveLength", label: "Sleeve", required: false, unit: "cm", placeholder: "e.g. 64", category: "body" },
|
|
10177
|
-
{ key: "inseam", label: "Inseam", required: false, unit: "cm", placeholder: "e.g. 81", category: "body" },
|
|
10178
|
-
{ key: "footLengthCm", label: "Foot length", required: false, unit: "cm", placeholder: "e.g. 27", category: "shoe" }
|
|
10179
|
-
];
|
|
10180
|
-
const TOTAL_STEPS = 3;
|
|
10181
|
-
const LS_PREFIX = "primestyle_";
|
|
10182
|
-
const PROFILES_KEY = "profiles";
|
|
10183
|
-
const ACTIVE_PROFILE_KEY = "active_profile_id";
|
|
10184
|
-
const PS_STORAGE_CHANGE_EVENT = "ps:storage-change";
|
|
10185
|
-
function emitStorageChange(reason) {
|
|
10186
|
-
if (typeof window === "undefined") return;
|
|
10187
|
-
try {
|
|
10188
|
-
window.dispatchEvent(new CustomEvent(PS_STORAGE_CHANGE_EVENT, { detail: { reason } }));
|
|
10189
|
-
} catch {
|
|
10190
|
-
}
|
|
10191
|
-
}
|
|
10192
|
-
function lsGet(key, fallback) {
|
|
10193
|
-
if (typeof window === "undefined") return fallback;
|
|
10194
|
-
try {
|
|
10195
|
-
const raw = localStorage.getItem(LS_PREFIX + key);
|
|
10196
|
-
return raw ? JSON.parse(raw) : fallback;
|
|
10197
|
-
} catch {
|
|
10198
|
-
return fallback;
|
|
10199
|
-
}
|
|
10200
|
-
}
|
|
10201
|
-
function lsSet(key, value) {
|
|
10202
|
-
try {
|
|
10203
|
-
localStorage.setItem(LS_PREFIX + key, JSON.stringify(value));
|
|
10204
|
-
} catch {
|
|
10205
|
-
}
|
|
10206
|
-
}
|
|
10207
|
-
function getProfiles() {
|
|
10208
|
-
return lsGet(PROFILES_KEY, []);
|
|
10209
|
-
}
|
|
10210
|
-
function saveProfiles(profiles) {
|
|
10211
|
-
lsSet(PROFILES_KEY, profiles);
|
|
10212
|
-
emitStorageChange("profiles");
|
|
10213
|
-
}
|
|
10214
|
-
function getActiveProfileId() {
|
|
10215
|
-
return lsGet(ACTIVE_PROFILE_KEY, null);
|
|
10216
|
-
}
|
|
10217
|
-
function setActiveProfileId(id2) {
|
|
10218
|
-
lsSet(ACTIVE_PROFILE_KEY, id2);
|
|
10219
|
-
emitStorageChange("active-profile");
|
|
10220
|
-
}
|
|
10221
|
-
function getActiveProfile() {
|
|
10222
|
-
const profiles = getProfiles();
|
|
10223
|
-
if (profiles.length === 0) return null;
|
|
10224
|
-
const activeId = getActiveProfileId();
|
|
10225
|
-
if (activeId) {
|
|
10226
|
-
const found = profiles.find((p2) => p2.id === activeId);
|
|
10227
|
-
if (found) return found;
|
|
10228
|
-
}
|
|
10229
|
-
const sorted = [...profiles].sort(
|
|
10230
|
-
(a, b) => (b.lastUsedAt || b.createdAt || 0) - (a.lastUsedAt || a.createdAt || 0)
|
|
10231
|
-
);
|
|
10232
|
-
return sorted[0] || null;
|
|
10233
|
-
}
|
|
10234
|
-
function updateProfile(id2, patch) {
|
|
10235
|
-
const profiles = getProfiles();
|
|
10236
|
-
const idx = profiles.findIndex((p2) => p2.id === id2);
|
|
10237
|
-
if (idx < 0) return null;
|
|
10238
|
-
const updated = { ...profiles[idx], ...patch, lastEditedAt: Date.now() };
|
|
10239
|
-
profiles[idx] = updated;
|
|
10240
|
-
saveProfiles(profiles);
|
|
10241
|
-
return updated;
|
|
10242
|
-
}
|
|
10243
|
-
function updateProfileMeasurements(id2, measurements, unit = "cm") {
|
|
10244
|
-
return updateProfile(id2, { measurements, measurementsUnit: unit });
|
|
10245
|
-
}
|
|
10246
|
-
function addSizeToHistory(profileId, entry) {
|
|
10247
|
-
const profiles = getProfiles();
|
|
10248
|
-
const idx = profiles.findIndex((p2) => p2.id === profileId);
|
|
10249
|
-
if (idx < 0) return null;
|
|
10250
|
-
const profile = profiles[idx];
|
|
10251
|
-
const history = (profile.sizeHistory || []).filter(
|
|
10252
|
-
(h) => h.productId !== entry.productId
|
|
10253
|
-
);
|
|
10254
|
-
history.unshift(entry);
|
|
10255
|
-
const trimmed = history.slice(0, 50);
|
|
10256
|
-
profiles[idx] = {
|
|
10257
|
-
...profile,
|
|
10258
|
-
sizeHistory: trimmed,
|
|
10259
|
-
lastUsedAt: Date.now()
|
|
10260
|
-
};
|
|
10261
|
-
saveProfiles(profiles);
|
|
10262
|
-
return profiles[idx];
|
|
10263
|
-
}
|
|
10264
|
-
function detectLocale() {
|
|
10265
|
-
if (typeof navigator === "undefined") return "US";
|
|
10266
|
-
const lang = navigator.language || "";
|
|
10267
|
-
const region = lang.split("-")[1]?.toUpperCase();
|
|
10268
|
-
if (region === "GB") return "UK";
|
|
10269
|
-
if (region) return region;
|
|
10270
|
-
const map = {
|
|
10271
|
-
en: "US",
|
|
10272
|
-
ja: "JP",
|
|
10273
|
-
ko: "KR",
|
|
10274
|
-
zh: "CN",
|
|
10275
|
-
fr: "FR",
|
|
10276
|
-
it: "IT",
|
|
10277
|
-
de: "DE",
|
|
10278
|
-
es: "ES",
|
|
10279
|
-
pt: "BR"
|
|
10280
|
-
};
|
|
10281
|
-
return map[lang.split("-")[0].toLowerCase()] || "US";
|
|
10282
|
-
}
|
|
10283
|
-
function getApiKey() {
|
|
10284
|
-
let key = "";
|
|
10285
|
-
try {
|
|
10286
|
-
key = "shopify-proxy";
|
|
10287
|
-
} catch {
|
|
10288
|
-
}
|
|
10289
|
-
if (!key) throw new PrimeStyleError("Missing NEXT_PUBLIC_PRIMESTYLE_API_KEY", "MISSING_API_KEY");
|
|
10290
|
-
return key;
|
|
10291
|
-
}
|
|
10292
|
-
function getApiUrl(override) {
|
|
10293
|
-
if (override) return override;
|
|
10294
|
-
let envUrl = "";
|
|
10295
|
-
try {
|
|
10296
|
-
envUrl = "";
|
|
10297
|
-
} catch {
|
|
10298
|
-
}
|
|
10299
|
-
return envUrl || "http://localhost:4000";
|
|
10300
|
-
}
|
|
10301
|
-
let cachedMP = null;
|
|
10302
|
-
const MP_CACHE_TTL_MS = 6e4;
|
|
10303
|
-
function setCachedMediaPipe(landmarks) {
|
|
10304
|
-
cachedMP = { landmarks, storedAt: Date.now() };
|
|
10305
|
-
}
|
|
10306
|
-
function getCachedMediaPipe() {
|
|
10307
|
-
if (!cachedMP) return null;
|
|
10308
|
-
if (Date.now() - cachedMP.storedAt > MP_CACHE_TTL_MS) {
|
|
10309
|
-
cachedMP = null;
|
|
10310
|
-
return null;
|
|
10311
|
-
}
|
|
10312
|
-
return cachedMP;
|
|
10313
|
-
}
|
|
10314
|
-
async function recommendForProduct(input) {
|
|
10315
|
-
const log = (...args) => console.log("[ps-sdk:recommend]", ...args);
|
|
10316
|
-
const profile = input.profile ?? getActiveProfile();
|
|
10317
|
-
if (!profile) {
|
|
10318
|
-
log("no active profile — returning null");
|
|
10319
|
-
return null;
|
|
10320
|
-
}
|
|
10321
|
-
log("start", {
|
|
10322
|
-
productId: input.productId,
|
|
10323
|
-
profileId: profile.id,
|
|
10324
|
-
profileName: profile.name,
|
|
10325
|
-
hasMeasurements: !!profile.measurements && Object.keys(profile.measurements || {}).length,
|
|
10326
|
-
measurementsCount: Object.keys(profile.measurements || {}).length
|
|
10327
|
-
});
|
|
10328
|
-
log("cache MISS — calling backend");
|
|
10329
|
-
let apiKey;
|
|
10330
|
-
try {
|
|
10331
|
-
apiKey = input.apiKey ?? getApiKey();
|
|
10332
|
-
} catch (e) {
|
|
10333
|
-
log("no api key — aborting", e);
|
|
10334
|
-
return null;
|
|
10335
|
-
}
|
|
10336
|
-
const apiUrl = (input.apiUrl ?? getApiUrl()).replace(/\/+$/, "");
|
|
10337
|
-
if (!apiKey) {
|
|
10338
|
-
log("no api key — aborting");
|
|
10339
|
-
return null;
|
|
10340
|
-
}
|
|
10341
|
-
let sizeGuide = null;
|
|
10342
|
-
if (input.sizeGuideData != null) {
|
|
10343
|
-
try {
|
|
10344
|
-
const sgRes = await fetch(`${apiUrl}/api/v1/sizing/sizeguide`, {
|
|
10345
|
-
method: "POST",
|
|
10346
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
10347
|
-
body: JSON.stringify({
|
|
10348
|
-
product: { title: input.productTitle, productId: input.productId },
|
|
10349
|
-
sizeGuideRaw: input.sizeGuideData
|
|
10350
|
-
})
|
|
10351
|
-
});
|
|
10352
|
-
if (sgRes.ok) {
|
|
10353
|
-
sizeGuide = await sgRes.json();
|
|
10354
|
-
log("sizeguide OK", { found: sizeGuide?.found, sectionCount: Object.keys(sizeGuide?.sections || {}).length });
|
|
10355
|
-
} else {
|
|
10356
|
-
log("sizeguide FAILED", sgRes.status, sgRes.statusText);
|
|
10357
|
-
}
|
|
10358
|
-
} catch (e) {
|
|
10359
|
-
log("sizeguide threw", e);
|
|
10360
|
-
}
|
|
10361
|
-
} else {
|
|
10362
|
-
log("no sizeGuideData provided");
|
|
10363
|
-
}
|
|
10364
|
-
const measurements = {
|
|
10365
|
-
gender: profile.gender,
|
|
10366
|
-
sizingUnit: profile.measurementsUnit || "cm"
|
|
10367
|
-
};
|
|
10368
|
-
if (profile.measurements) {
|
|
10369
|
-
for (const [key, value] of Object.entries(profile.measurements)) {
|
|
10370
|
-
if (value != null) measurements[key] = value;
|
|
10371
|
-
}
|
|
10372
|
-
}
|
|
10373
|
-
if (profile.height != null) measurements.height = profile.height;
|
|
10374
|
-
if (profile.weight != null) measurements.weight = profile.weight;
|
|
10375
|
-
if (profile.heightUnit) measurements.heightUnit = profile.heightUnit;
|
|
10376
|
-
if (profile.weightUnit) measurements.weightUnit = profile.weightUnit;
|
|
10377
|
-
if (profile.age) measurements.age = profile.age;
|
|
10378
|
-
if (profile.chestProfile) measurements.chestProfile = profile.chestProfile;
|
|
10379
|
-
if (profile.midsectionProfile) measurements.midsectionProfile = profile.midsectionProfile;
|
|
10380
|
-
if (profile.hipProfile) measurements.hipProfile = profile.hipProfile;
|
|
10381
|
-
const payload = {
|
|
10382
|
-
method: "exact",
|
|
10383
|
-
locale: profile.country || "US",
|
|
10384
|
-
sizingUnit: profile.measurementsUnit || profile.sizingUnit || "cm",
|
|
10385
|
-
product: { title: input.productTitle, productId: input.productId, description: "", variants: [] },
|
|
10386
|
-
measurements
|
|
10387
|
-
};
|
|
10388
|
-
if (profile.gender === "female" && profile.bandSize && profile.cupSize) {
|
|
10389
|
-
const region = profile.braRegion ?? profile.braSizeRegion ?? "US";
|
|
10390
|
-
payload.braSize = {
|
|
10391
|
-
band: parseFloat(String(profile.bandSize)),
|
|
10392
|
-
cup: String(profile.cupSize),
|
|
10393
|
-
region
|
|
10394
|
-
};
|
|
10395
|
-
}
|
|
10396
|
-
if (profile.knownMeasurements) {
|
|
10397
|
-
payload.knownMeasurements = profile.knownMeasurements;
|
|
10398
|
-
}
|
|
10399
|
-
if (sizeGuide && sizeGuide.found) {
|
|
10400
|
-
payload.sizeGuide = sizeGuide;
|
|
10401
|
-
}
|
|
10402
|
-
log("calling /sizing/recommend", { measurements: Object.keys(measurements), hasSizeGuide: !!payload.sizeGuide });
|
|
10403
|
-
let result = null;
|
|
10404
|
-
try {
|
|
10405
|
-
const res = await fetch(`${apiUrl}/api/v1/sizing/recommend`, {
|
|
10406
|
-
method: "POST",
|
|
10407
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
10408
|
-
body: JSON.stringify(payload)
|
|
10409
|
-
});
|
|
10410
|
-
if (!res.ok) {
|
|
10411
|
-
log("recommend FAILED", res.status, res.statusText);
|
|
10412
|
-
return null;
|
|
10413
|
-
}
|
|
10414
|
-
result = await res.json();
|
|
10415
|
-
log("recommend OK", {
|
|
10416
|
-
recommendedSize: result?.recommendedSize,
|
|
10417
|
-
sectionKeys: result?.sections ? Object.keys(result.sections) : null,
|
|
10418
|
-
sections: result?.sections
|
|
10419
|
-
});
|
|
10420
|
-
} catch (e) {
|
|
10421
|
-
log("recommend threw", e);
|
|
10422
|
-
return null;
|
|
10423
|
-
}
|
|
10424
|
-
if (!result || !result.recommendedSize) {
|
|
10425
|
-
log("recommend returned no recommendedSize — bailing");
|
|
10426
|
-
return null;
|
|
10427
|
-
}
|
|
10428
|
-
const sectionsMap = result.sections ? Object.fromEntries(
|
|
10429
|
-
Object.entries(result.sections).map(([name, sec]) => [name, sec.recommendedSize])
|
|
10430
|
-
) : void 0;
|
|
10431
|
-
const sectionsFull = result.sections ? Object.fromEntries(
|
|
10432
|
-
Object.entries(result.sections).map(([name, sec]) => [
|
|
10433
|
-
name,
|
|
10434
|
-
{
|
|
10435
|
-
recommendedSize: sec.recommendedSize,
|
|
10436
|
-
// Backend may include these on merged Jacket/Pants entries
|
|
10437
|
-
size: sec.size,
|
|
10438
|
-
length: sec.length,
|
|
10439
|
-
availableSizes: sec.availableSizes,
|
|
10440
|
-
availableLengths: sec.availableLengths
|
|
10441
|
-
}
|
|
10442
|
-
])
|
|
10443
|
-
) : void 0;
|
|
10444
|
-
addSizeToHistory(profile.id, {
|
|
10445
|
-
productId: input.productId,
|
|
10446
|
-
productTitle: input.productTitle,
|
|
10447
|
-
productImage: input.productImage,
|
|
10448
|
-
recommendedSize: result.recommendedSize,
|
|
10449
|
-
confidence: result.confidence,
|
|
10450
|
-
sections: sectionsMap,
|
|
10451
|
-
sectionsFull,
|
|
10452
|
-
recommendedLength: result.recommendedLength || void 0,
|
|
10453
|
-
savedAt: Date.now()
|
|
10454
|
-
});
|
|
10455
|
-
return {
|
|
10456
|
-
recommendedSize: result.recommendedSize,
|
|
10457
|
-
confidence: result.confidence,
|
|
10458
|
-
sections: sectionsMap,
|
|
10459
|
-
profileId: profile.id,
|
|
10460
|
-
fromCache: false,
|
|
10461
|
-
raw: result,
|
|
10462
|
-
found: result.found
|
|
10463
|
-
};
|
|
10464
|
-
}
|
|
10465
|
-
async function estimateFullMeasurements(args) {
|
|
10466
|
-
let apiKey;
|
|
10467
|
-
try {
|
|
10468
|
-
apiKey = args.apiKey ?? getApiKey();
|
|
10469
|
-
} catch {
|
|
10470
|
-
return null;
|
|
10471
|
-
}
|
|
10472
|
-
const apiUrl = (args.apiUrl ?? getApiUrl()).replace(/\/+$/, "");
|
|
10473
|
-
if (!apiKey) return null;
|
|
10474
|
-
const requiredFields = [
|
|
10475
|
-
"chest",
|
|
10476
|
-
"bust",
|
|
10477
|
-
"waist",
|
|
10478
|
-
"hips",
|
|
10479
|
-
"shoulderWidth",
|
|
10480
|
-
"sleeveLength",
|
|
10481
|
-
"inseam",
|
|
10482
|
-
"neckCircumference",
|
|
10483
|
-
"thighCircumference",
|
|
10484
|
-
"footLengthCm"
|
|
10485
|
-
];
|
|
10486
|
-
const payload = {
|
|
10487
|
-
height: args.height ?? 0,
|
|
10488
|
-
weight: args.weight ?? 0,
|
|
10489
|
-
heightUnit: args.heightUnit,
|
|
10490
|
-
weightUnit: args.weightUnit,
|
|
10491
|
-
gender: args.gender,
|
|
10492
|
-
requiredFields
|
|
10493
|
-
};
|
|
10494
|
-
if (args.age) payload.age = args.age;
|
|
10495
|
-
if (args.chestProfile) payload.chestProfile = args.chestProfile;
|
|
10496
|
-
if (args.midsectionProfile) payload.midsectionProfile = args.midsectionProfile;
|
|
10497
|
-
if (args.hipProfile) payload.hipProfile = args.hipProfile;
|
|
10498
|
-
if (args.bodyImage) payload.bodyImage = args.bodyImage;
|
|
10499
|
-
let landmarksForPayload = args.bodyLandmarks ?? null;
|
|
10500
|
-
if (!landmarksForPayload) {
|
|
10501
|
-
const cached = getCachedMediaPipe();
|
|
10502
|
-
if (cached) {
|
|
10503
|
-
landmarksForPayload = cached.landmarks;
|
|
10504
|
-
console.log("[PS-SDK:estimate] reusing cached MediaPipe result from recent /recommend run");
|
|
10505
|
-
}
|
|
10506
|
-
}
|
|
10507
|
-
if (!landmarksForPayload && args.bodyImage) {
|
|
10508
|
-
console.log("[PS-SDK:estimate] bodyImage present but bodyLandmarks missing — running MediaPipe now");
|
|
10509
|
-
try {
|
|
10510
|
-
const detected = await detectBodyLandmarks(args.bodyImage);
|
|
10511
|
-
if (detected) {
|
|
10512
|
-
landmarksForPayload = detected;
|
|
10513
|
-
console.log("[PS-SDK:estimate] MediaPipe auto-detect ✓ anchors available for backend");
|
|
10514
|
-
} else {
|
|
10515
|
-
console.warn("[PS-SDK:estimate] MediaPipe auto-detect returned null — backend will fall back to low-confidence visual estimation");
|
|
10516
|
-
}
|
|
10517
|
-
} catch (e) {
|
|
10518
|
-
console.warn("[PS-SDK:estimate] MediaPipe auto-detect threw:", e);
|
|
10519
|
-
}
|
|
10520
|
-
}
|
|
10521
|
-
if (landmarksForPayload) payload.bodyLandmarks = landmarksForPayload;
|
|
10522
|
-
if (landmarksForPayload) {
|
|
10523
|
-
const lm = landmarksForPayload;
|
|
10524
|
-
const required = [
|
|
10525
|
-
["leftShoulder", lm.leftShoulder?.visibility],
|
|
10526
|
-
["rightShoulder", lm.rightShoulder?.visibility],
|
|
10527
|
-
["leftHip", lm.leftHip?.visibility],
|
|
10528
|
-
["rightHip", lm.rightHip?.visibility]
|
|
10529
|
-
];
|
|
10530
|
-
const lowVis = required.filter(([, v2]) => typeof v2 === "number" && v2 < 0.5);
|
|
10531
|
-
if (lowVis.length > 0) {
|
|
10532
|
-
console.warn(`[PS-SDK:estimate] LOW joint visibility — ${lowVis.map(([k2, v2]) => `${k2}=${v2.toFixed(2)}`).join(", ")} — accuracy may suffer; ask user to retake with better framing/lighting`);
|
|
10533
|
-
}
|
|
10534
|
-
}
|
|
10535
|
-
try {
|
|
10536
|
-
console.groupCollapsed(`[PS-SDK:estimate] POST /api/v1/sizing/estimate`);
|
|
10537
|
-
console.log("url:", `${apiUrl}/api/v1/sizing/estimate`);
|
|
10538
|
-
console.log("payload (sans bodyImage):", {
|
|
10539
|
-
...payload,
|
|
10540
|
-
bodyImage: payload.bodyImage ? `[base64 ~${Math.round(String(payload.bodyImage).length / 1024)}KB]` : void 0,
|
|
10541
|
-
bodyLandmarks: payload.bodyLandmarks ? {
|
|
10542
|
-
count: Object.keys(payload.bodyLandmarks).length,
|
|
10543
|
-
imageWidth: payload.bodyLandmarks.imageWidth,
|
|
10544
|
-
imageHeight: payload.bodyLandmarks.imageHeight
|
|
10545
|
-
} : void 0
|
|
10546
|
-
});
|
|
10547
|
-
console.groupEnd();
|
|
10548
|
-
{
|
|
10549
|
-
const lmObj = payload.bodyLandmarks;
|
|
10550
|
-
const jointCount = lmObj ? Object.values(lmObj).filter(
|
|
10551
|
-
(v2) => v2 && typeof v2 === "object" && typeof v2.x === "number" && typeof v2.y === "number"
|
|
10552
|
-
).length : 0;
|
|
10553
|
-
console.log(`[ps-sdk:debug] /estimate payload → bodyLandmarks=${!!lmObj}(${jointCount} joints)`);
|
|
10554
|
-
}
|
|
10555
|
-
const res = await fetch(`${apiUrl}/api/v1/sizing/estimate`, {
|
|
10556
|
-
method: "POST",
|
|
10557
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
10558
|
-
body: JSON.stringify(payload)
|
|
10559
|
-
});
|
|
10560
|
-
if (!res.ok) {
|
|
10561
|
-
console.warn(`[PS-SDK:estimate] ✗ ${res.status} ${res.statusText}`);
|
|
10562
|
-
return null;
|
|
10563
|
-
}
|
|
10564
|
-
const data = await res.json();
|
|
10565
|
-
console.groupCollapsed(`[PS-SDK:estimate] ✓ response (confidence=${data?.confidence ?? "—"})`);
|
|
10566
|
-
console.log("estimates:", data.estimates);
|
|
10567
|
-
console.log("userEstimates:", data.userEstimates);
|
|
10568
|
-
console.log("method:", data.method, "unit:", data.unit);
|
|
10569
|
-
console.log("full response:", data);
|
|
10570
|
-
console.groupEnd();
|
|
10571
|
-
if (!data?.estimates) return null;
|
|
10572
|
-
return {
|
|
10573
|
-
estimates: data.estimates,
|
|
10574
|
-
unit: data.unit || "cm",
|
|
10575
|
-
userEstimates: data.userEstimates
|
|
10576
|
-
};
|
|
10577
|
-
} catch (err) {
|
|
10578
|
-
console.error("[PS-SDK:estimate] threw:", err);
|
|
10579
|
-
return null;
|
|
10580
|
-
}
|
|
10581
|
-
}
|
|
10582
|
-
function isImperial(locale) {
|
|
10583
|
-
return ["US", "UK", "AU"].includes(locale);
|
|
10584
|
-
}
|
|
10585
|
-
function getUnitLabel(unit) {
|
|
10586
|
-
if (unit === "in" || unit === "inches" || unit === "lbs") return "Imperial";
|
|
10587
|
-
if (unit === "cm" || unit === "kg") return "Metric";
|
|
10588
|
-
if (unit === "mm") return "mm";
|
|
10589
|
-
return "";
|
|
10590
|
-
}
|
|
10591
|
-
const cache = /* @__PURE__ */ new Map();
|
|
10592
|
-
function scoreLandmarks(lm) {
|
|
10593
|
-
if (!lm) return 0;
|
|
10594
|
-
let joints = 0;
|
|
10595
|
-
for (const [k2, v2] of Object.entries(lm)) {
|
|
10596
|
-
if (k2 === "imageWidth" || k2 === "imageHeight") continue;
|
|
10597
|
-
if (v2 && typeof v2 === "object" && typeof v2.x === "number") joints++;
|
|
10598
|
-
}
|
|
10599
|
-
let score = joints * 10;
|
|
10600
|
-
if (lm.nose) score += 50;
|
|
10601
|
-
if (lm.leftAnkle && lm.rightAnkle) score += 5;
|
|
10602
|
-
return score;
|
|
10603
|
-
}
|
|
10604
|
-
async function scoreImage(url) {
|
|
10605
|
-
try {
|
|
10606
|
-
const lm = await detectBodyLandmarks(url);
|
|
10607
|
-
return scoreLandmarks(lm);
|
|
10608
|
-
} catch {
|
|
10609
|
-
return 0;
|
|
10610
|
-
}
|
|
10611
|
-
}
|
|
10612
|
-
async function pickBestGarmentImage(images) {
|
|
10613
|
-
if (!images || !images.length) return null;
|
|
10614
|
-
if (images.length === 1) return images[0];
|
|
10615
|
-
const cacheKey = images.join("|");
|
|
10616
|
-
const cached = cache.get(cacheKey);
|
|
10617
|
-
if (cached) return cached;
|
|
10618
|
-
const t0 = Date.now();
|
|
10619
|
-
const scored = await Promise.all(images.map(async (url) => ({
|
|
10620
|
-
url,
|
|
10621
|
-
score: await scoreImage(url)
|
|
10622
|
-
})));
|
|
10623
|
-
scored.sort((a, b) => b.score - a.score);
|
|
10624
|
-
const best = (scored[0]?.score ?? 0) > 0 ? scored[0].url : images[0];
|
|
10625
|
-
cache.set(cacheKey, best);
|
|
10626
|
-
console.log(`[ps-sdk:garment-pick] ${Date.now() - t0}ms — chose ${images.indexOf(best)}/${images.length}`);
|
|
10627
|
-
for (const s of scored) console.log(`[ps-sdk:garment-pick] ${s.score.toString().padStart(4, " ")} ${s.url}`);
|
|
10628
|
-
return best;
|
|
10629
|
-
}
|
|
10630
|
-
function cx(base, override) {
|
|
10631
|
-
return override ? `${base} ${override}` : base;
|
|
10632
|
-
}
|
|
10633
|
-
const STYLES$1 = `
|
|
10634
|
-
/* Variable defaults must live on BOTH the root (for the trigger button)
|
|
10635
|
-
and the overlay (which is React-portaled to <body> and therefore not
|
|
10636
|
-
a descendant of .ps-tryon-root, so the cascade is broken). Without
|
|
10637
|
-
this duplication the slider track gradient and the thumb border —
|
|
10638
|
-
both var(--ps-accent) — render as unset and become invisible. */
|
|
10639
|
-
.ps-tryon-root,
|
|
10640
|
-
.ps-tryon-overlay {
|
|
10641
|
-
--ps-accent: #3B82F6;
|
|
10642
|
-
--ps-accent-hover: #2563EB;
|
|
10643
|
-
--ps-accent-light: #BFDBFE;
|
|
10644
|
-
--ps-text-primary: #1C1D1E;
|
|
10645
|
-
--ps-text-secondary: #454545;
|
|
10646
|
-
--ps-text-muted: #84898C;
|
|
10647
|
-
--ps-text-dim: #696E71;
|
|
10648
|
-
--ps-border-color: #E7E7E7;
|
|
10649
|
-
--ps-border-subtle: #F0F0F0;
|
|
10650
|
-
--ps-bg-primary: #FFFFFF;
|
|
10651
|
-
--ps-bg-secondary: #F8FAFF;
|
|
10652
|
-
--ps-bg-tertiary: #F0F0F0;
|
|
10653
|
-
--ps-input-bg: #FFFFFF;
|
|
10654
|
-
--ps-input-border: #E7E7E7;
|
|
10655
|
-
--ps-input-focus-border: var(--ps-accent);
|
|
10656
|
-
--ps-error-color: #E7000B;
|
|
10657
|
-
--ps-success-color: #1C9D4C;
|
|
10658
|
-
--ps-warning-color: #DF8400;
|
|
10659
|
-
--ps-logo-height: 2.8vw;
|
|
10660
|
-
}
|
|
10661
|
-
.ps-tryon-root { display: inline-block; visibility: visible !important; }
|
|
10662
|
-
|
|
10663
|
-
.ps-tryon-btn {
|
|
10664
|
-
display: inline-flex; align-items: center; gap: max(6px, 0.42vw);
|
|
10665
|
-
padding: var(--ps-btn-padding, max(10px, 0.63vw) max(16px, 1.25vw));
|
|
10666
|
-
background: var(--ps-btn-bg, var(--ps-accent)); color: var(--ps-btn-color, #FFFFFF);
|
|
10667
|
-
font-family: var(--ps-btn-font, system-ui, -apple-system, sans-serif);
|
|
10668
|
-
font-size: var(--ps-btn-font-size, max(13px, 0.73vw)); font-weight: var(--ps-btn-font-weight, 600);
|
|
10669
|
-
border: var(--ps-btn-border, none); border-radius: var(--ps-btn-radius, max(6px, 0.42vw));
|
|
10670
|
-
cursor: pointer; transition: all 0.2s ease;
|
|
10671
|
-
width: var(--ps-btn-width, auto); height: var(--ps-btn-height, auto);
|
|
10672
|
-
box-shadow: var(--ps-btn-shadow, none); line-height: 1; white-space: nowrap;
|
|
10673
|
-
box-sizing: border-box;
|
|
10674
|
-
}
|
|
10675
|
-
.ps-tryon-btn svg { width: var(--ps-btn-icon-size, max(16px, 1.1vw)); height: var(--ps-btn-icon-size, max(16px, 1.1vw)); flex-shrink: 0; }
|
|
10676
|
-
.ps-tryon-btn:hover { background: var(--ps-btn-hover-bg, var(--ps-accent-hover)); transform: translateY(-1px); }
|
|
10677
|
-
.ps-tryon-btn:active { transform: translateY(0); }
|
|
10678
|
-
|
|
10679
|
-
.ps-tryon-overlay {
|
|
10680
|
-
position: fixed; inset: 0; background: var(--ps-modal-overlay, rgba(0,0,0,0.6));
|
|
10681
|
-
display: flex; align-items: center; justify-content: center;
|
|
10682
|
-
z-index: 2147483647;
|
|
10683
|
-
isolation: isolate;
|
|
10684
|
-
contain: layout style;
|
|
10685
|
-
padding: 0.83vw;
|
|
10686
|
-
padding: max(0.83vw, env(safe-area-inset-top)) max(0.83vw, env(safe-area-inset-right)) max(0.83vw, env(safe-area-inset-bottom)) max(0.83vw, env(safe-area-inset-left));
|
|
10687
|
-
animation: ps-fade-in 0.2s ease;
|
|
10688
|
-
overflow-y: auto; -webkit-overflow-scrolling: touch;
|
|
10689
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
10690
|
-
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
10691
|
-
box-sizing: border-box;
|
|
10692
|
-
}
|
|
10693
|
-
.ps-tryon-overlay, .ps-tryon-overlay *, .ps-tryon-overlay *::before, .ps-tryon-overlay *::after { box-sizing: border-box; }
|
|
10694
|
-
@keyframes ps-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
10695
|
-
|
|
10696
|
-
.ps-tryon-modal {
|
|
10697
|
-
background: var(--ps-modal-bg, #FFFFFF); color: var(--ps-modal-color, #1C1D1E);
|
|
10698
|
-
border-radius: clamp(8px, 0.83vw, 16px); width: var(--ps-modal-width, 100%);
|
|
10699
|
-
max-width: var(--ps-modal-max-width, 36vw); max-height: 92dvh; overflow-y: auto;
|
|
10700
|
-
font-family: var(--ps-modal-font, system-ui, -apple-system, sans-serif);
|
|
10701
|
-
box-shadow: 0 8px 40px rgba(0,0,0,0.12); animation: ps-slide-up 0.3s ease;
|
|
10702
|
-
scrollbar-width: thin; scrollbar-color: var(--ps-border-color) transparent;
|
|
10703
|
-
flex-shrink: 0;
|
|
10704
|
-
container-type: inline-size;
|
|
10705
|
-
}
|
|
10706
|
-
.ps-tryon-modal-wide {
|
|
10707
|
-
/* Use the SMALLER of 55vw or 92dvh×(16/10) for width so the modal
|
|
10708
|
-
never gets wider than the height can support. Height bumped from
|
|
10709
|
-
70dvh → 82dvh to fit the longer accessory photo step (guidelines +
|
|
10710
|
-
legal notice + nav) without pushing content under the Analyze CTA. */
|
|
10711
|
-
width: min(55vw, calc(92dvh * 1.6));
|
|
10712
|
-
max-width: min(55vw, calc(92dvh * 1.6));
|
|
10713
|
-
height: min(82dvh, calc(55vw / 1.35));
|
|
10714
|
-
max-height: min(92dvh, calc(55vw / 1.35));
|
|
10715
|
-
display: flex; flex-direction: column; overflow: hidden;
|
|
10716
|
-
transition: height 0.45s cubic-bezier(0.32, 0.72, 0, 1), max-height 0.45s cubic-bezier(0.32, 0.72, 0, 1);
|
|
10717
|
-
}
|
|
10718
|
-
.ps-tryon-modal-tall.ps-tryon-modal-wide {
|
|
10719
|
-
height: min(92dvh, calc(55vw / 1.2));
|
|
10720
|
-
max-height: min(95dvh, calc(55vw / 1.2));
|
|
10721
|
-
}
|
|
10722
|
-
.ps-tryon-modal:has(.ps-tryon-drawer-open) { overflow: hidden; }
|
|
10723
|
-
@keyframes ps-slide-up { from { transform: translateY(12px) scale(0.97); opacity: 0; filter: blur(4px); } to { transform: none; opacity: 1; filter: none; } }
|
|
10724
|
-
|
|
10725
|
-
/* Header */
|
|
10726
|
-
.ps-tryon-header {
|
|
10727
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
10728
|
-
padding: max(8px, 0.7vw) max(12px, 1.25vw);
|
|
10729
|
-
background: var(--ps-modal-header-bg, rgba(255,255,255,0.95));
|
|
10730
|
-
border-bottom: 1px solid var(--ps-border-color);
|
|
10731
|
-
border-radius: max(8px, 0.83vw) max(8px, 0.83vw) 0 0;
|
|
10732
|
-
backdrop-filter: blur(12px); flex-shrink: 0;
|
|
10733
|
-
}
|
|
10734
|
-
.ps-tryon-header-minimal {
|
|
10735
|
-
justify-content: flex-end; gap: 0.4vw; padding: 0.4vw 0.8vw;
|
|
10736
|
-
}
|
|
10737
|
-
.ps-tryon-back-btn {
|
|
10738
|
-
display: flex; align-items: center; gap: 0.35vw;
|
|
10739
|
-
background: none; border: none; color: var(--ps-text-secondary); cursor: pointer;
|
|
10740
|
-
font-size: 0.73vw; padding: 0; margin-bottom: 0.63vw;
|
|
10741
|
-
transition: color 0.2s;
|
|
10742
|
-
}
|
|
10743
|
-
.ps-tryon-back-btn:hover { color: var(--ps-text-primary); }
|
|
10744
|
-
.ps-tryon-back-btn svg { width: 1vw; height: 0.63vw; stroke: currentColor; flex-shrink: 0; }
|
|
10745
|
-
|
|
10746
|
-
/* Regen progress bar — absolutely-positioned fill div inside the
|
|
10747
|
-
"Generating new try-on…" button. Animation runs at the browser's
|
|
10748
|
-
refresh rate (smooth) instead of via React state ticks. The button
|
|
10749
|
-
itself sets position: relative + overflow: hidden so the fill is
|
|
10750
|
-
clipped to the button's rounded shape. */
|
|
10751
|
-
@keyframes ps-tryon-regen-fill {
|
|
10752
|
-
from { width: 0%; }
|
|
10753
|
-
to { width: 100%; }
|
|
10754
|
-
}
|
|
10755
|
-
.ps-tryon-regen-fill {
|
|
10756
|
-
position: absolute;
|
|
10757
|
-
left: 0; top: 0; bottom: 0;
|
|
10758
|
-
width: 0%;
|
|
10759
|
-
background: rgba(255, 255, 255, 0.28);
|
|
10760
|
-
animation: ps-tryon-regen-fill 22s linear forwards;
|
|
10761
|
-
pointer-events: none;
|
|
10762
|
-
z-index: 0;
|
|
10763
|
-
}
|
|
10764
|
-
.ps-tryon-title { display: flex; align-items: center; }
|
|
10765
|
-
.ps-tryon-logo-img { height: var(--ps-logo-height); width: auto; }
|
|
10766
|
-
.ps-tryon-header-actions { display: flex; align-items: center; gap: 0.42vw; }
|
|
10767
|
-
.ps-tryon-header-icon {
|
|
10768
|
-
/* Pure vw sizing collapsed to ~8 px on 375 px mobile. Clamp so the icon
|
|
10769
|
-
stays finger-tappable (min 30 px) while scaling up on large screens. */
|
|
10770
|
-
width: clamp(30px, 2.2vw, 34px); height: clamp(30px, 2.2vw, 34px);
|
|
10771
|
-
display: flex; align-items: center; justify-content: center;
|
|
10772
|
-
border: 1.5px solid var(--ps-border-color); border-radius: clamp(6px, 0.52vw, 10px); background: transparent;
|
|
10773
|
-
cursor: pointer; color: var(--ps-text-secondary); transition: all 0.2s;
|
|
10774
|
-
}
|
|
10775
|
-
.ps-tryon-header-icon:hover { border-color: var(--ps-accent); color: var(--ps-accent); }
|
|
10776
|
-
.ps-tryon-header-icon svg { stroke: currentColor; fill: none; width: clamp(14px, 0.9vw, 16px); height: clamp(14px, 0.9vw, 16px); }
|
|
10777
|
-
.ps-tryon-close {
|
|
10778
|
-
width: 2.2vw; height: 2.2vw; display: flex; align-items: center; justify-content: center;
|
|
10779
|
-
background: none; border: none; color: var(--ps-modal-close-color, #999);
|
|
10780
|
-
cursor: pointer; border-radius: 0.31vw; transition: background 0.15s;
|
|
10781
|
-
}
|
|
10782
|
-
.ps-tryon-close:hover { background: rgba(0,0,0,0.06); }
|
|
10783
|
-
.ps-tryon-close svg { width: 1vw; height: 1vw; }
|
|
10784
|
-
|
|
10785
|
-
/* Language switcher */
|
|
10786
|
-
.ps-tryon-lang-wrap { position: relative; z-index: 99999; }
|
|
10787
|
-
.ps-tryon-lang-trigger {
|
|
10788
|
-
display: flex; align-items: center; gap: clamp(5px, 0.36vw, 7px);
|
|
10789
|
-
padding: clamp(6px, 0.36vw, 8px) clamp(10px, 0.73vw, 14px);
|
|
10790
|
-
border: 1.5px solid var(--ps-border-color); border-radius: clamp(8px, 0.57vw, 10px);
|
|
10791
|
-
background: transparent; cursor: pointer; color: var(--ps-text-secondary);
|
|
10792
|
-
transition: all 0.25s ease; font-family: inherit; white-space: nowrap;
|
|
10793
|
-
min-height: clamp(28px, 2.2vw, 38px);
|
|
10794
|
-
}
|
|
10795
|
-
.ps-tryon-lang-trigger:hover, .ps-tryon-lang-trigger.ps-active {
|
|
10796
|
-
border-color: var(--ps-accent); color: var(--ps-accent); background: rgba(33,84,239,0.06);
|
|
10797
|
-
}
|
|
10798
|
-
.ps-tryon-lang-trigger svg { stroke: currentColor; fill: none; flex-shrink: 0; width: clamp(13px, 0.8vw, 16px); height: clamp(13px, 0.8vw, 16px); }
|
|
10799
|
-
.ps-tryon-lang-current {
|
|
10800
|
-
font-size: clamp(12px, 0.68vw, 14px); font-weight: 500; letter-spacing: 0.01em;
|
|
10801
|
-
}
|
|
10802
|
-
.ps-tryon-lang-arrow {
|
|
10803
|
-
font-size: clamp(10px, 0.57vw, 12px); transition: transform 0.25s ease; display: inline-block;
|
|
10804
|
-
}
|
|
10805
|
-
.ps-tryon-lang-arrow.ps-open { transform: rotate(180deg); }
|
|
10806
|
-
|
|
10807
|
-
/* Dropdown is portaled to document.body — uses hardcoded colors (no CSS vars available) */
|
|
10808
|
-
.ps-tryon-lang-dropdown {
|
|
10809
|
-
min-width: min(13vw, 180px);
|
|
10810
|
-
background: #FFFFFF; border: 1.5px solid rgba(0,0,0,0.1);
|
|
10811
|
-
border-radius: clamp(8px, 0.73vw, 12px);
|
|
10812
|
-
box-shadow: 0 clamp(6px, 0.5vw, 10px) clamp(16px, 2vw, 32px) rgba(0,0,0,0.15),
|
|
10813
|
-
0 0 0 1px rgba(0,0,0,0.03);
|
|
10814
|
-
overflow: hidden;
|
|
10815
|
-
animation: ps-lang-open 0.2s ease both;
|
|
10816
|
-
font-family: system-ui, -apple-system, sans-serif;
|
|
9431
|
+
.ps-tryon-lang-arrow.ps-open { transform: rotate(180deg); }
|
|
9432
|
+
|
|
9433
|
+
/* Dropdown is portaled to document.body — uses hardcoded colors (no CSS vars available) */
|
|
9434
|
+
.ps-tryon-lang-dropdown {
|
|
9435
|
+
min-width: min(13vw, 180px);
|
|
9436
|
+
background: #FFFFFF; border: 1.5px solid rgba(0,0,0,0.1);
|
|
9437
|
+
border-radius: clamp(8px, 0.73vw, 12px);
|
|
9438
|
+
box-shadow: 0 clamp(6px, 0.5vw, 10px) clamp(16px, 2vw, 32px) rgba(0,0,0,0.15),
|
|
9439
|
+
0 0 0 1px rgba(0,0,0,0.03);
|
|
9440
|
+
overflow: hidden;
|
|
9441
|
+
animation: ps-lang-open 0.2s ease both;
|
|
9442
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
10817
9443
|
}
|
|
10818
9444
|
@keyframes ps-lang-open {
|
|
10819
9445
|
from { opacity: 0; transform: translateY(-0.42vw) scale(0.96); }
|
|
@@ -17403,850 +16029,2224 @@ const STYLES$1 = `
|
|
|
17403
16029
|
border: 1.5px solid rgba(255, 255, 255, 0.35);
|
|
17404
16030
|
border-top-color: #FFFFFF;
|
|
17405
16031
|
border-radius: 50%;
|
|
17406
|
-
animation: ps-pmv-spin 0.8s linear infinite;
|
|
16032
|
+
animation: ps-pmv-spin 0.8s linear infinite;
|
|
16033
|
+
}
|
|
16034
|
+
|
|
16035
|
+
/* Big product / try-on image */
|
|
16036
|
+
.ps-msd-image {
|
|
16037
|
+
width: 100%; height: 420px;
|
|
16038
|
+
border-radius: 12px; overflow: hidden;
|
|
16039
|
+
background: var(--ps-bg-secondary);
|
|
16040
|
+
display: flex; align-items: center; justify-content: center;
|
|
16041
|
+
flex-shrink: 0;
|
|
16042
|
+
}
|
|
16043
|
+
.ps-msd-image-img {
|
|
16044
|
+
width: 100%; height: 100%;
|
|
16045
|
+
object-fit: contain; display: block;
|
|
16046
|
+
}
|
|
16047
|
+
|
|
16048
|
+
/* RECOMMENDED SIZE / FIT ACCURACY card */
|
|
16049
|
+
.ps-msd-card {
|
|
16050
|
+
background: var(--ps-bg-primary);
|
|
16051
|
+
border: 1px solid var(--ps-border-subtle);
|
|
16052
|
+
border-radius: 12px;
|
|
16053
|
+
padding: 22px 22px 18px;
|
|
16054
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
16055
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
16056
|
+
}
|
|
16057
|
+
.ps-msd-card-eyebrow {
|
|
16058
|
+
font-size: 10px; font-weight: 700;
|
|
16059
|
+
letter-spacing: 0.16em; text-transform: uppercase;
|
|
16060
|
+
color: var(--ps-text-muted);
|
|
16061
|
+
}
|
|
16062
|
+
.ps-msd-card-size-row {
|
|
16063
|
+
display: flex; align-items: baseline; gap: 12px;
|
|
16064
|
+
margin-top: 2px;
|
|
16065
|
+
}
|
|
16066
|
+
.ps-msd-card-size {
|
|
16067
|
+
font-size: 56px; font-weight: 700;
|
|
16068
|
+
color: var(--ps-text-primary);
|
|
16069
|
+
line-height: 1; letter-spacing: -0.02em;
|
|
16070
|
+
font-feature-settings: "tnum" 1;
|
|
16071
|
+
}
|
|
16072
|
+
.ps-msd-card-size-meta {
|
|
16073
|
+
font-size: 12px; font-weight: 600;
|
|
16074
|
+
letter-spacing: 0.05em;
|
|
16075
|
+
color: var(--ps-text-muted);
|
|
16076
|
+
}
|
|
16077
|
+
.ps-msd-card-divider {
|
|
16078
|
+
height: 1px; background: var(--ps-border-subtle);
|
|
16079
|
+
margin: 6px 0;
|
|
16080
|
+
}
|
|
16081
|
+
.ps-msd-card-pct-row {
|
|
16082
|
+
display: flex; align-items: center; gap: 12px;
|
|
16083
|
+
margin-top: 2px;
|
|
16084
|
+
}
|
|
16085
|
+
.ps-msd-card-pct {
|
|
16086
|
+
font-size: 30px; font-weight: 700;
|
|
16087
|
+
color: var(--ps-accent);
|
|
16088
|
+
line-height: 1; font-feature-settings: "tnum" 1;
|
|
16089
|
+
}
|
|
16090
|
+
.ps-msd-card-bar {
|
|
16091
|
+
flex: 1; height: 2px;
|
|
16092
|
+
background: var(--ps-border-color); border-radius: 1px;
|
|
16093
|
+
overflow: hidden;
|
|
16094
|
+
}
|
|
16095
|
+
.ps-msd-card-bar-fill {
|
|
16096
|
+
height: 100%; background: var(--ps-accent);
|
|
16097
|
+
border-radius: 1px;
|
|
16098
|
+
}
|
|
16099
|
+
.ps-msd-card-note {
|
|
16100
|
+
font-size: 11px; color: var(--ps-text-muted);
|
|
16101
|
+
line-height: 1.5; margin: 2px 0 0;
|
|
16102
|
+
}
|
|
16103
|
+
|
|
16104
|
+
/* MEASUREMENT BLUEPRINT — rich rows with number, description, USER/SIZE
|
|
16105
|
+
labeled values, status badge with check icon */
|
|
16106
|
+
.ps-msd-blueprint {
|
|
16107
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
16108
|
+
padding-top: 6px;
|
|
16109
|
+
}
|
|
16110
|
+
.ps-msd-blueprint-title {
|
|
16111
|
+
font-size: 13px; font-weight: 800;
|
|
16112
|
+
letter-spacing: 0.12em; text-transform: uppercase;
|
|
16113
|
+
color: var(--ps-text-primary);
|
|
16114
|
+
margin-bottom: 0;
|
|
16115
|
+
}
|
|
16116
|
+
.ps-msd-blueprint-underline {
|
|
16117
|
+
width: 36px; height: 2px;
|
|
16118
|
+
background: var(--ps-text-primary);
|
|
16119
|
+
margin-bottom: 8px;
|
|
16120
|
+
}
|
|
16121
|
+
.ps-msd-rows {
|
|
16122
|
+
display: flex; flex-direction: column;
|
|
16123
|
+
}
|
|
16124
|
+
.ps-msd-row {
|
|
16125
|
+
display: flex; flex-direction: column;
|
|
16126
|
+
padding: 22px 0 24px;
|
|
16127
|
+
border-bottom: 1px solid var(--ps-border-subtle);
|
|
16128
|
+
}
|
|
16129
|
+
.ps-msd-row:last-child { border-bottom: none; }
|
|
16130
|
+
.ps-msd-row-num {
|
|
16131
|
+
font-size: 11px; font-weight: 700;
|
|
16132
|
+
letter-spacing: 0.14em; text-transform: uppercase;
|
|
16133
|
+
color: var(--ps-text-primary);
|
|
16134
|
+
margin-bottom: 4px;
|
|
16135
|
+
}
|
|
16136
|
+
.ps-msd-row-desc {
|
|
16137
|
+
font-size: 12px; font-weight: 400;
|
|
16138
|
+
color: var(--ps-text-muted);
|
|
16139
|
+
margin: 0 0 14px;
|
|
16140
|
+
line-height: 1.5;
|
|
16141
|
+
}
|
|
16142
|
+
.ps-msd-row-cells {
|
|
16143
|
+
display: flex; justify-content: space-between; align-items: flex-end;
|
|
16144
|
+
gap: 16px; margin-bottom: 14px;
|
|
16145
|
+
}
|
|
16146
|
+
.ps-msd-row-cell {
|
|
16147
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
16148
|
+
min-width: 0;
|
|
16149
|
+
}
|
|
16150
|
+
.ps-msd-row-cell.ps-right { text-align: right; }
|
|
16151
|
+
.ps-msd-cell-label {
|
|
16152
|
+
font-size: 9px; font-weight: 600;
|
|
16153
|
+
letter-spacing: 0.14em; text-transform: uppercase;
|
|
16154
|
+
color: var(--ps-text-muted);
|
|
16155
|
+
}
|
|
16156
|
+
.ps-msd-cell-value {
|
|
16157
|
+
font-size: 18px; font-weight: 700;
|
|
16158
|
+
color: var(--ps-text-primary);
|
|
16159
|
+
font-feature-settings: "tnum" 1;
|
|
16160
|
+
line-height: 1.1;
|
|
16161
|
+
}
|
|
16162
|
+
.ps-msd-row-badge {
|
|
16163
|
+
align-self: flex-end;
|
|
16164
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
16165
|
+
font-size: 10px; font-weight: 700;
|
|
16166
|
+
letter-spacing: 0.12em; text-transform: uppercase;
|
|
16167
|
+
white-space: nowrap;
|
|
16168
|
+
}
|
|
16169
|
+
.ps-msd-row-badge svg { flex-shrink: 0; }
|
|
16170
|
+
.ps-msd-row-badge.ps-good {
|
|
16171
|
+
color: var(--ps-accent);
|
|
16172
|
+
}
|
|
16173
|
+
.ps-msd-row-badge.ps-tight {
|
|
16174
|
+
color: #dc2626;
|
|
16175
|
+
}
|
|
16176
|
+
.ps-msd-row-badge.ps-loose {
|
|
16177
|
+
color: #d97706;
|
|
16178
|
+
}
|
|
16179
|
+
|
|
16180
|
+
/* TRY ANOTHER SIZE picker */
|
|
16181
|
+
.ps-msd-sizes {
|
|
16182
|
+
display: flex; flex-direction: column; gap: 10px;
|
|
16183
|
+
padding-top: 18px;
|
|
16184
|
+
}
|
|
16185
|
+
.ps-msd-sizes-label {
|
|
16186
|
+
font-size: 11px; font-weight: 700;
|
|
16187
|
+
letter-spacing: 0.14em; text-transform: uppercase;
|
|
16188
|
+
color: var(--ps-text-muted);
|
|
16189
|
+
text-align: center;
|
|
16190
|
+
}
|
|
16191
|
+
.ps-msd-sizes-pills {
|
|
16192
|
+
display: flex; flex-wrap: wrap; gap: 8px;
|
|
16193
|
+
justify-content: center;
|
|
16194
|
+
}
|
|
16195
|
+
.ps-msd-size-pill {
|
|
16196
|
+
min-width: 48px;
|
|
16197
|
+
padding: 10px 16px;
|
|
16198
|
+
background: transparent;
|
|
16199
|
+
border: 1.5px solid var(--ps-border-color);
|
|
16200
|
+
border-radius: 8px;
|
|
16201
|
+
font-family: inherit; font-size: 13px; font-weight: 600;
|
|
16202
|
+
color: var(--ps-text-primary);
|
|
16203
|
+
cursor: pointer;
|
|
16204
|
+
transition: all 0.15s;
|
|
16205
|
+
}
|
|
16206
|
+
.ps-msd-size-pill:hover {
|
|
16207
|
+
border-color: var(--ps-accent);
|
|
16208
|
+
}
|
|
16209
|
+
.ps-msd-size-pill.ps-active {
|
|
16210
|
+
background: var(--ps-accent); color: #FFFFFF;
|
|
16211
|
+
border-color: var(--ps-accent);
|
|
16212
|
+
}
|
|
16213
|
+
|
|
16214
|
+
/* Bottom action area — Next button + tab bar grouped together,
|
|
16215
|
+
pushed to the bottom of the modal body by .ps-bpm-spacer above. */
|
|
16216
|
+
.ps-bpm-bottom {
|
|
16217
|
+
display: flex; flex-direction: column;
|
|
16218
|
+
margin: 0 -16px;
|
|
16219
|
+
border-top: 1px solid var(--ps-border-subtle);
|
|
16220
|
+
background: var(--ps-bg-primary);
|
|
16221
|
+
flex-shrink: 0;
|
|
16222
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
16223
|
+
}
|
|
16224
|
+
.ps-bpm-next-btn {
|
|
16225
|
+
margin: max(14px, 3.5vw) max(16px, 4.1vw) max(12px, 3vw);
|
|
16226
|
+
padding: max(14px, 3.5vw) max(18px, 4.6vw);
|
|
16227
|
+
background: var(--ps-accent); color: #FFFFFF;
|
|
16228
|
+
border: none; border-radius: max(10px, 2.5vw);
|
|
16229
|
+
font-family: inherit; font-size: max(14px, 3.6vw); font-weight: 700;
|
|
16230
|
+
cursor: pointer; transition: opacity 0.15s, transform 0.15s;
|
|
16231
|
+
letter-spacing: 0.02em;
|
|
16232
|
+
}
|
|
16233
|
+
.ps-bpm-next-btn:active { transform: scale(0.98); }
|
|
16234
|
+
.ps-bpm-next-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
16235
|
+
.ps-bpm-bottom-tabs {
|
|
16236
|
+
display: flex; align-items: stretch;
|
|
16237
|
+
border-top: 1px solid var(--ps-border-subtle);
|
|
16238
|
+
background: var(--ps-bg-primary);
|
|
16239
|
+
}
|
|
16240
|
+
.ps-bpm-bottom-tab {
|
|
16241
|
+
flex: 1; background: none; border: none;
|
|
16242
|
+
padding: max(14px, 3.5vw) max(8px, 2vw) max(12px, 3vw);
|
|
16243
|
+
font-family: inherit; font-size: max(10px, 2.5vw); font-weight: 700;
|
|
16244
|
+
letter-spacing: 0.12em; text-transform: uppercase;
|
|
16245
|
+
color: var(--ps-text-muted); cursor: pointer;
|
|
16246
|
+
border-bottom: 2px solid transparent;
|
|
16247
|
+
transition: color 0.15s, border-color 0.15s;
|
|
16248
|
+
display: flex; align-items: center; justify-content: center; gap: max(6px, 1.5vw);
|
|
16249
|
+
}
|
|
16250
|
+
.ps-bpm-bottom-tab.ps-active {
|
|
16251
|
+
color: var(--ps-accent);
|
|
16252
|
+
border-bottom-color: var(--ps-accent);
|
|
16253
|
+
}
|
|
16254
|
+
.ps-bpm-bottom-tab svg { width: max(13px, 3.3vw); height: max(13px, 3.3vw); flex-shrink: 0; }
|
|
16255
|
+
|
|
16256
|
+
.ps-bp-optional { font-weight: 400; color: var(--ps-text-muted); }
|
|
16257
|
+
|
|
16258
|
+
/* Bra size step — vertically centered */
|
|
16259
|
+
.ps-bp-bra-step { display: flex; flex-direction: column; justify-content: center; flex: 1; }
|
|
16260
|
+
.ps-bp-bra-step .ps-bp-title { text-align: center; margin-bottom: 1.5vw; }
|
|
16261
|
+
|
|
16262
|
+
/* Bra size selector */
|
|
16263
|
+
.ps-bp-bra-section { margin-bottom: 1.2vw; }
|
|
16264
|
+
.ps-bp-bra-header {
|
|
16265
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
16266
|
+
margin-bottom: 0.5vw;
|
|
16267
|
+
}
|
|
16268
|
+
.ps-bp-bra-label {
|
|
16269
|
+
font-size: 0.6vw; font-weight: 600; color: rgba(255,255,255,0.45);
|
|
16270
|
+
letter-spacing: 0.08em; text-transform: uppercase;
|
|
16271
|
+
}
|
|
16272
|
+
/* Region dropdown */
|
|
16273
|
+
/* ── Photo upload overlay (shown during compress + age-check) ── */
|
|
16274
|
+
.ps-bp-photo-overlay {
|
|
16275
|
+
position: absolute;
|
|
16276
|
+
inset: 0;
|
|
16277
|
+
background: rgba(255, 255, 255, 0.92);
|
|
16278
|
+
display: flex;
|
|
16279
|
+
flex-direction: column;
|
|
16280
|
+
align-items: center;
|
|
16281
|
+
justify-content: center;
|
|
16282
|
+
gap: 0.9vw;
|
|
16283
|
+
z-index: 10;
|
|
16284
|
+
border-radius: inherit;
|
|
16285
|
+
backdrop-filter: blur(2px);
|
|
16286
|
+
animation: ps-bp-fade 0.18s ease both;
|
|
16287
|
+
}
|
|
16288
|
+
.ps-bp-photo-overlay-spinner {
|
|
16289
|
+
width: 2.2vw;
|
|
16290
|
+
height: 2.2vw;
|
|
16291
|
+
min-width: 28px;
|
|
16292
|
+
min-height: 28px;
|
|
16293
|
+
border-radius: 50%;
|
|
16294
|
+
border: 2.5px solid rgba(33, 84, 239, 0.18);
|
|
16295
|
+
border-top-color: var(--ps-accent);
|
|
16296
|
+
animation: ps-bp-spin 0.7s linear infinite;
|
|
17407
16297
|
}
|
|
17408
|
-
|
|
17409
|
-
|
|
17410
|
-
.ps-msd-image {
|
|
17411
|
-
width: 100%; height: 420px;
|
|
17412
|
-
border-radius: 12px; overflow: hidden;
|
|
17413
|
-
background: var(--ps-bg-secondary);
|
|
17414
|
-
display: flex; align-items: center; justify-content: center;
|
|
17415
|
-
flex-shrink: 0;
|
|
16298
|
+
@keyframes ps-bp-spin {
|
|
16299
|
+
to { transform: rotate(360deg); }
|
|
17416
16300
|
}
|
|
17417
|
-
.ps-
|
|
17418
|
-
|
|
17419
|
-
|
|
16301
|
+
.ps-bp-photo-overlay-label {
|
|
16302
|
+
font-size: max(12px, 0.85vw);
|
|
16303
|
+
font-weight: 600;
|
|
16304
|
+
color: var(--ps-text-primary);
|
|
16305
|
+
letter-spacing: 0.01em;
|
|
17420
16306
|
}
|
|
17421
16307
|
|
|
17422
|
-
/*
|
|
17423
|
-
.ps-
|
|
17424
|
-
|
|
16308
|
+
/* ── Photo-help popover (Do / Don't / Tip) — desktop default ── */
|
|
16309
|
+
.ps-bp-photo-help-backdrop { display: none; }
|
|
16310
|
+
.ps-bp-photo-help {
|
|
16311
|
+
position: absolute; top: 2.2vw; right: 0;
|
|
16312
|
+
width: min(360px, 22vw);
|
|
16313
|
+
background: #FFFFFF;
|
|
17425
16314
|
border: 1px solid var(--ps-border-subtle);
|
|
17426
|
-
border-radius:
|
|
17427
|
-
|
|
17428
|
-
|
|
17429
|
-
|
|
16315
|
+
border-radius: 0.8vw;
|
|
16316
|
+
box-shadow: 0 20px 40px -12px rgba(17,24,39,0.25), 0 8px 16px -8px rgba(17,24,39,0.15);
|
|
16317
|
+
padding: 0.8vw 1vw;
|
|
16318
|
+
display: flex; flex-direction: column; gap: 0.6vw;
|
|
16319
|
+
z-index: 5;
|
|
17430
16320
|
}
|
|
17431
|
-
.ps-
|
|
17432
|
-
|
|
17433
|
-
|
|
17434
|
-
|
|
16321
|
+
.ps-bp-photo-help-handle { display: none; }
|
|
16322
|
+
.ps-bp-photo-help-header { display: flex; align-items: center; justify-content: space-between; }
|
|
16323
|
+
.ps-bp-photo-help-title { font-size: 0.8vw; font-weight: 700; color: var(--ps-text-primary); }
|
|
16324
|
+
.ps-bp-photo-help-close {
|
|
16325
|
+
width: 1.4vw; height: 1.4vw; border-radius: 50%;
|
|
16326
|
+
background: transparent; border: none; color: var(--ps-text-muted);
|
|
16327
|
+
cursor: pointer; font-size: 1vw; line-height: 1;
|
|
17435
16328
|
}
|
|
17436
|
-
.ps-
|
|
17437
|
-
|
|
17438
|
-
|
|
16329
|
+
.ps-bp-photo-help-section { border-radius: 0.5vw; padding: 0.55vw 0.75vw; }
|
|
16330
|
+
.ps-bp-photo-help-section-label { font-size: 0.7vw; font-weight: 700; margin-bottom: 0.3vw; }
|
|
16331
|
+
.ps-bp-photo-help-section-body { font-size: 0.6vw; color: var(--ps-text-primary); line-height: 1.6; }
|
|
16332
|
+
.ps-bp-photo-help-do { background: #ddfbe7; }
|
|
16333
|
+
.ps-bp-photo-help-do .ps-bp-photo-help-section-label { color: #1c9d4c; }
|
|
16334
|
+
.ps-bp-photo-help-dont { background: #ffe2e2; }
|
|
16335
|
+
.ps-bp-photo-help-dont .ps-bp-photo-help-section-label { color: #e7000b; }
|
|
16336
|
+
.ps-bp-photo-help-tip {
|
|
16337
|
+
background: #c6e1f7; font-size: 0.6vw; line-height: 1.55;
|
|
16338
|
+
color: var(--ps-text-primary);
|
|
17439
16339
|
}
|
|
17440
|
-
.ps-
|
|
17441
|
-
|
|
16340
|
+
.ps-bp-photo-help-tip strong { color: #3267c3; }
|
|
16341
|
+
|
|
16342
|
+
/* ── MiniSelect: compact animated dropdown for inline bra inputs ── */
|
|
16343
|
+
.ps-bp-mini-select {
|
|
16344
|
+
position: relative;
|
|
16345
|
+
display: inline-flex;
|
|
16346
|
+
width: 100%;
|
|
16347
|
+
}
|
|
16348
|
+
.ps-bp-mini-select-trigger {
|
|
16349
|
+
display: flex;
|
|
16350
|
+
align-items: center;
|
|
16351
|
+
justify-content: space-between;
|
|
16352
|
+
gap: 0.4vw;
|
|
16353
|
+
width: 100%;
|
|
16354
|
+
min-width: 4vw;
|
|
16355
|
+
padding: 0.45vw 0.7vw;
|
|
16356
|
+
background: var(--ps-bg-secondary, #fff);
|
|
16357
|
+
border: 1.5px solid rgba(0, 0, 0, 0.12);
|
|
16358
|
+
border-radius: 0.4vw;
|
|
17442
16359
|
color: var(--ps-text-primary);
|
|
17443
|
-
|
|
17444
|
-
font-
|
|
16360
|
+
font-size: max(11px, 0.78vw);
|
|
16361
|
+
font-weight: 600;
|
|
16362
|
+
font-family: inherit;
|
|
16363
|
+
cursor: pointer;
|
|
16364
|
+
transition: border-color 0.18s, background 0.18s, transform 0.12s;
|
|
17445
16365
|
}
|
|
17446
|
-
.ps-
|
|
17447
|
-
|
|
17448
|
-
letter-spacing: 0.05em;
|
|
17449
|
-
color: var(--ps-text-muted);
|
|
16366
|
+
.ps-bp-mini-select-trigger:hover {
|
|
16367
|
+
border-color: var(--ps-accent);
|
|
17450
16368
|
}
|
|
17451
|
-
.ps-
|
|
17452
|
-
|
|
17453
|
-
|
|
16369
|
+
.ps-bp-mini-select[data-open="true"] .ps-bp-mini-select-trigger {
|
|
16370
|
+
border-color: var(--ps-accent);
|
|
16371
|
+
background: rgba(33, 84, 239, 0.04);
|
|
17454
16372
|
}
|
|
17455
|
-
.ps-
|
|
17456
|
-
|
|
17457
|
-
|
|
16373
|
+
.ps-bp-mini-select-value { display: inline-flex; align-items: center; }
|
|
16374
|
+
.ps-bp-mini-select-placeholder {
|
|
16375
|
+
color: var(--ps-text-secondary);
|
|
16376
|
+
opacity: 0.7;
|
|
16377
|
+
font-weight: 500;
|
|
17458
16378
|
}
|
|
17459
|
-
.ps-
|
|
17460
|
-
font-size:
|
|
16379
|
+
.ps-bp-mini-select-arrow {
|
|
16380
|
+
font-size: 0.55vw;
|
|
16381
|
+
color: var(--ps-text-secondary);
|
|
16382
|
+
transition: transform 0.22s ease;
|
|
16383
|
+
}
|
|
16384
|
+
.ps-bp-mini-select-arrow.ps-open {
|
|
16385
|
+
transform: rotate(180deg);
|
|
17461
16386
|
color: var(--ps-accent);
|
|
17462
|
-
line-height: 1; font-feature-settings: "tnum" 1;
|
|
17463
16387
|
}
|
|
17464
|
-
.ps-
|
|
17465
|
-
|
|
17466
|
-
|
|
17467
|
-
|
|
16388
|
+
.ps-bp-mini-select-panel {
|
|
16389
|
+
position: absolute;
|
|
16390
|
+
left: 0;
|
|
16391
|
+
right: 0;
|
|
16392
|
+
max-height: 9vw;
|
|
16393
|
+
overflow-y: auto;
|
|
16394
|
+
background: var(--ps-bg-secondary, #fff);
|
|
16395
|
+
border: 1.5px solid rgba(0, 0, 0, 0.08);
|
|
16396
|
+
border-radius: 0.45vw;
|
|
16397
|
+
box-shadow:
|
|
16398
|
+
0 0.25vw 0.55vw rgba(15, 23, 42, 0.08),
|
|
16399
|
+
0 0.85vw 2.0vw rgba(15, 23, 42, 0.14);
|
|
16400
|
+
padding: 0.25vw;
|
|
16401
|
+
z-index: 250;
|
|
17468
16402
|
}
|
|
17469
|
-
.ps-
|
|
17470
|
-
|
|
17471
|
-
|
|
16403
|
+
.ps-bp-mini-select[data-dir="down"] .ps-bp-mini-select-panel {
|
|
16404
|
+
top: calc(100% + 0.3vw);
|
|
16405
|
+
transform-origin: top center;
|
|
16406
|
+
animation: ps-bp-mini-slide-down 0.18s cubic-bezier(0.2, 0.8, 0.4, 1) both;
|
|
17472
16407
|
}
|
|
17473
|
-
.ps-
|
|
17474
|
-
|
|
17475
|
-
|
|
16408
|
+
.ps-bp-mini-select[data-dir="up"] .ps-bp-mini-select-panel {
|
|
16409
|
+
bottom: calc(100% + 0.3vw);
|
|
16410
|
+
transform-origin: bottom center;
|
|
16411
|
+
animation: ps-bp-mini-slide-up 0.18s cubic-bezier(0.2, 0.8, 0.4, 1) both;
|
|
17476
16412
|
}
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
.ps-msd-blueprint {
|
|
17481
|
-
display: flex; flex-direction: column; gap: 6px;
|
|
17482
|
-
padding-top: 6px;
|
|
16413
|
+
@keyframes ps-bp-mini-slide-down {
|
|
16414
|
+
from { opacity: 0; transform: translateY(-0.35vw) scale(0.96); }
|
|
16415
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
17483
16416
|
}
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
16417
|
+
@keyframes ps-bp-mini-slide-up {
|
|
16418
|
+
from { opacity: 0; transform: translateY(0.35vw) scale(0.96); }
|
|
16419
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
16420
|
+
}
|
|
16421
|
+
.ps-bp-mini-select-item {
|
|
16422
|
+
display: block;
|
|
16423
|
+
width: 100%;
|
|
16424
|
+
padding: 0.45vw 0.7vw;
|
|
16425
|
+
background: transparent;
|
|
16426
|
+
border: none;
|
|
16427
|
+
border-radius: 0.35vw;
|
|
16428
|
+
color: var(--ps-text-secondary);
|
|
16429
|
+
font-size: max(11px, 0.78vw);
|
|
16430
|
+
font-weight: 600;
|
|
16431
|
+
font-family: inherit;
|
|
16432
|
+
text-align: left;
|
|
16433
|
+
cursor: pointer;
|
|
16434
|
+
transition: background 0.12s, color 0.12s, transform 0.12s;
|
|
16435
|
+
}
|
|
16436
|
+
.ps-bp-mini-select-item:hover {
|
|
16437
|
+
background: rgba(33, 84, 239, 0.06);
|
|
17487
16438
|
color: var(--ps-text-primary);
|
|
17488
|
-
margin-bottom: 0;
|
|
17489
16439
|
}
|
|
17490
|
-
.ps-
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
margin-bottom: 8px;
|
|
16440
|
+
.ps-bp-mini-select-item.ps-selected {
|
|
16441
|
+
background: rgba(33, 84, 239, 0.12);
|
|
16442
|
+
color: var(--ps-accent);
|
|
17494
16443
|
}
|
|
17495
|
-
|
|
17496
|
-
|
|
16444
|
+
|
|
16445
|
+
.ps-bp-bra-region-wrap { position: relative; z-index: 100; }
|
|
16446
|
+
.ps-bp-bra-region-trigger {
|
|
16447
|
+
display: flex; align-items: center; gap: 0.35vw;
|
|
16448
|
+
padding: 0.45vw 0.9vw; border: 1.5px solid rgba(0,0,0,0.1);
|
|
16449
|
+
border-radius: 0.4vw; background: transparent;
|
|
16450
|
+
color: var(--ps-text-secondary); font-size: 0.8vw; font-weight: 600;
|
|
16451
|
+
cursor: pointer; font-family: inherit; transition: all 0.25s;
|
|
17497
16452
|
}
|
|
17498
|
-
.ps-
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
border-bottom: 1px solid var(--ps-border-subtle);
|
|
16453
|
+
.ps-bp-bra-region-trigger:hover { border-color: var(--ps-accent); color: var(--ps-accent); }
|
|
16454
|
+
.ps-bp-bra-region-arrow {
|
|
16455
|
+
font-size: 0.55vw; transition: transform 0.25s;
|
|
17502
16456
|
}
|
|
17503
|
-
.ps-
|
|
17504
|
-
.ps-
|
|
17505
|
-
|
|
17506
|
-
|
|
17507
|
-
|
|
17508
|
-
|
|
16457
|
+
.ps-bp-bra-region-arrow.ps-open { transform: rotate(180deg); }
|
|
16458
|
+
.ps-bp-bra-region-dropdown {
|
|
16459
|
+
position: absolute; top: calc(100% + 0.3vw); right: 0;
|
|
16460
|
+
min-width: 5vw; background: var(--ps-bg-secondary);
|
|
16461
|
+
border: 1.5px solid rgba(0,0,0,0.08); border-radius: 0.4vw;
|
|
16462
|
+
box-shadow: 0 0.5vw 1.5vw rgba(0,0,0,0.5);
|
|
16463
|
+
overflow: hidden; animation: ps-bp-fade 0.2s ease both;
|
|
16464
|
+
padding: 0.25vw; z-index: 200;
|
|
17509
16465
|
}
|
|
17510
|
-
.ps-
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
|
|
17514
|
-
|
|
16466
|
+
.ps-bp-bra-region-item {
|
|
16467
|
+
display: block; width: 100%; padding: 0.4vw 0.7vw;
|
|
16468
|
+
background: transparent; border: none; border-radius: 0.3vw;
|
|
16469
|
+
color: var(--ps-text-secondary); font-size: 0.75vw; font-weight: 600;
|
|
16470
|
+
cursor: pointer; font-family: inherit; text-align: left;
|
|
16471
|
+
transition: all 0.15s;
|
|
17515
16472
|
}
|
|
17516
|
-
.ps-
|
|
17517
|
-
|
|
17518
|
-
|
|
16473
|
+
.ps-bp-bra-region-item:hover { background: rgba(0,0,0,0.04); color: var(--ps-text-primary); }
|
|
16474
|
+
.ps-bp-bra-region-item.ps-selected { color: var(--ps-accent); background: rgba(33,84,239,0.1); }
|
|
16475
|
+
.ps-bp-bra-grid {
|
|
16476
|
+
display: grid; grid-template-columns: repeat(7, 1fr); gap: 0.35vw;
|
|
16477
|
+
}
|
|
16478
|
+
.ps-bp-bra-btn {
|
|
16479
|
+
padding: 0.4vw 0; text-align: center;
|
|
16480
|
+
background: transparent; border: 1px solid rgba(0,0,0,0.08);
|
|
16481
|
+
border-radius: 0.35vw; color: var(--ps-text-secondary);
|
|
16482
|
+
font-size: 0.65vw; font-weight: 500; cursor: pointer;
|
|
16483
|
+
font-family: inherit; transition: all 0.2s;
|
|
17519
16484
|
}
|
|
17520
|
-
.ps-
|
|
17521
|
-
|
|
17522
|
-
|
|
16485
|
+
.ps-bp-bra-btn:hover { border-color: rgba(0,0,0,0.3); color: var(--ps-text-primary); }
|
|
16486
|
+
.ps-bp-bra-btn-selected {
|
|
16487
|
+
background: var(--ps-accent) !important; border-color: var(--ps-accent) !important;
|
|
16488
|
+
color: #fff !important; font-weight: 700;
|
|
17523
16489
|
}
|
|
17524
|
-
|
|
17525
|
-
.ps-
|
|
17526
|
-
|
|
17527
|
-
|
|
17528
|
-
color: var(--ps-text-muted);
|
|
16490
|
+
/* Bra inline result — shown in header next to region switcher */
|
|
16491
|
+
.ps-bp-bra-inline-result {
|
|
16492
|
+
margin-left: auto; font-size: max(11px, 0.75vw); color: var(--ps-accent);
|
|
16493
|
+
font-weight: 600; white-space: nowrap;
|
|
17529
16494
|
}
|
|
17530
|
-
.ps-
|
|
17531
|
-
font-
|
|
17532
|
-
color: var(--ps-text-primary);
|
|
17533
|
-
font-feature-settings: "tnum" 1;
|
|
17534
|
-
line-height: 1.1;
|
|
16495
|
+
.ps-bp-bra-inline-result strong {
|
|
16496
|
+
font-weight: 700;
|
|
17535
16497
|
}
|
|
17536
|
-
|
|
17537
|
-
|
|
17538
|
-
|
|
17539
|
-
|
|
17540
|
-
|
|
17541
|
-
white-space: nowrap;
|
|
16498
|
+
|
|
16499
|
+
/* Profile option rows — WAIR style stacked list */
|
|
16500
|
+
.ps-bp-options {
|
|
16501
|
+
display: flex; flex-direction: column; gap: 0.5vw;
|
|
16502
|
+
margin-top: 0.8vw;
|
|
17542
16503
|
}
|
|
17543
|
-
.ps-
|
|
17544
|
-
|
|
17545
|
-
|
|
16504
|
+
.ps-bp-option {
|
|
16505
|
+
display: flex; align-items: center; gap: 0.8vw;
|
|
16506
|
+
padding: 0.35vw 1vw 0.35vw 0.35vw;
|
|
16507
|
+
background: #fff; border: 1.5px solid rgba(0,0,0,0.06);
|
|
16508
|
+
border-radius: 0.6vw; cursor: pointer;
|
|
16509
|
+
transition: all 0.25s ease; font-family: inherit;
|
|
16510
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
|
17546
16511
|
}
|
|
17547
|
-
.ps-
|
|
17548
|
-
color:
|
|
16512
|
+
.ps-bp-option:hover {
|
|
16513
|
+
border-color: rgba(33,84,239,0.5);
|
|
16514
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
16515
|
+
transform: translateY(-1px);
|
|
17549
16516
|
}
|
|
17550
|
-
.ps-
|
|
17551
|
-
|
|
16517
|
+
.ps-bp-option:hover .ps-bp-illust svg { transform: scale(1.05); }
|
|
16518
|
+
.ps-bp-option-selected {
|
|
16519
|
+
border-color: var(--ps-accent) !important;
|
|
16520
|
+
box-shadow: 0 4px 12px rgba(33,84,239,0.18) !important;
|
|
17552
16521
|
}
|
|
17553
|
-
|
|
17554
|
-
|
|
17555
|
-
|
|
17556
|
-
|
|
17557
|
-
padding-top: 18px;
|
|
16522
|
+
.ps-bp-option-icon {
|
|
16523
|
+
width: 6vw; height: 6vw; flex-shrink: 0;
|
|
16524
|
+
display: flex; align-items: center; justify-content: center;
|
|
16525
|
+
border-radius: 0.4vw; overflow: hidden;
|
|
17558
16526
|
}
|
|
17559
|
-
.ps-
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
|
|
17563
|
-
text-align: center;
|
|
16527
|
+
.ps-bp-illust { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
|
16528
|
+
.ps-bp-illust svg { width: 100%; height: 100%; transition: transform 0.25s ease; }
|
|
16529
|
+
.ps-bp-option-label {
|
|
16530
|
+
flex: 1; font-size: 0.73vw; font-weight: 500;
|
|
16531
|
+
color: #555; text-align: center;
|
|
17564
16532
|
}
|
|
17565
|
-
.ps-
|
|
17566
|
-
|
|
17567
|
-
|
|
16533
|
+
.ps-bp-option-selected .ps-bp-option-label { color: var(--ps-accent); font-weight: 600; }
|
|
16534
|
+
.ps-bp-option::after {
|
|
16535
|
+
content: "›"; font-size: 1.1vw; color: var(--ps-text-secondary); flex-shrink: 0;
|
|
16536
|
+
transition: all 0.25s ease;
|
|
17568
16537
|
}
|
|
17569
|
-
.ps-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
17573
|
-
|
|
17574
|
-
|
|
17575
|
-
|
|
17576
|
-
|
|
17577
|
-
|
|
17578
|
-
|
|
16538
|
+
.ps-bp-option:hover::after { color: var(--ps-accent); transform: translateX(2px); }
|
|
16539
|
+
.ps-bp-option-selected::after { color: var(--ps-accent); }
|
|
16540
|
+
|
|
16541
|
+
/* Single question step — 3 cards centered */
|
|
16542
|
+
.ps-bp-single-q { display: flex; flex-direction: column; flex: 1; justify-content: center; align-items: center; }
|
|
16543
|
+
.ps-bp-single-q .ps-bp-title { margin-bottom: 1vw; width: 100%; text-align: center; font-size: 1.05vw; font-weight: 700; letter-spacing: 0.01em; }
|
|
16544
|
+
.ps-bp-shape-row-full { width: 100%; margin: 1.5vw auto 0; }
|
|
16545
|
+
.ps-bp-shape-row-full .ps-bp-img-card { max-height: 18vw; padding: 0.8vw 0.6vw 0; }
|
|
16546
|
+
.ps-bp-shape-row-full .ps-bp-img-card-thumb { height: 13vw; }
|
|
16547
|
+
|
|
16548
|
+
/* Body shapes screen — fit all questions without scrolling */
|
|
16549
|
+
.ps-bp-shapes-fit { display: flex; flex-direction: column; flex: 1; overflow: hidden; gap: 0.4vw; justify-content: flex-start !important; }
|
|
16550
|
+
|
|
16551
|
+
/* Body shapes combined screen */
|
|
16552
|
+
.ps-bp-shape-section {
|
|
16553
|
+
margin-bottom: 0.4vw; flex: 1; display: flex; flex-direction: column; min-height: 0;
|
|
17579
16554
|
}
|
|
17580
|
-
.ps-
|
|
17581
|
-
|
|
16555
|
+
.ps-bp-shape-label {
|
|
16556
|
+
font-size: 0.55vw; font-weight: 500; color: rgba(255,255,255,0.7);
|
|
16557
|
+
margin-bottom: 0.3vw; display: flex; align-items: center; gap: 0.6vw;
|
|
16558
|
+
text-transform: uppercase; letter-spacing: 0.05em;
|
|
17582
16559
|
}
|
|
17583
|
-
.ps-
|
|
17584
|
-
|
|
17585
|
-
|
|
16560
|
+
.ps-bp-shape-label::after {
|
|
16561
|
+
content: ""; flex: 1; height: 1px;
|
|
16562
|
+
background: linear-gradient(to right, rgba(0,0,0,0.08), transparent);
|
|
17586
16563
|
}
|
|
17587
|
-
|
|
17588
|
-
|
|
17589
|
-
|
|
17590
|
-
.ps-bpm-bottom {
|
|
17591
|
-
display: flex; flex-direction: column;
|
|
17592
|
-
margin: 0 -16px;
|
|
17593
|
-
border-top: 1px solid var(--ps-border-subtle);
|
|
17594
|
-
background: var(--ps-bg-primary);
|
|
17595
|
-
flex-shrink: 0;
|
|
17596
|
-
padding-bottom: env(safe-area-inset-bottom);
|
|
16564
|
+
.ps-bp-shape-row {
|
|
16565
|
+
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.5vw;
|
|
16566
|
+
flex: 1; min-height: 0;
|
|
17597
16567
|
}
|
|
17598
|
-
.ps-
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
|
|
17602
|
-
|
|
17603
|
-
|
|
17604
|
-
cursor: pointer; transition: opacity 0.15s, transform 0.15s;
|
|
17605
|
-
letter-spacing: 0.02em;
|
|
16568
|
+
.ps-bp-img-card {
|
|
16569
|
+
display: flex; flex-direction: column; align-items: center;
|
|
16570
|
+
background: transparent; border: 2px solid rgba(0,0,0,0.08);
|
|
16571
|
+
border-radius: 0.5vw; overflow: hidden; cursor: pointer;
|
|
16572
|
+
transition: all 0.2s ease; padding: 0; font-family: inherit;
|
|
16573
|
+
position: relative;
|
|
17606
16574
|
}
|
|
17607
|
-
.ps-
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
display: flex; align-items: stretch;
|
|
17611
|
-
border-top: 1px solid var(--ps-border-subtle);
|
|
17612
|
-
background: var(--ps-bg-primary);
|
|
16575
|
+
.ps-bp-img-card:hover {
|
|
16576
|
+
border-color: rgba(33,84,239,0.4);
|
|
16577
|
+
transform: translateY(-0.1vw);
|
|
17613
16578
|
}
|
|
17614
|
-
.ps-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
font-family: inherit; font-size: max(10px, 2.5vw); font-weight: 700;
|
|
17618
|
-
letter-spacing: 0.12em; text-transform: uppercase;
|
|
17619
|
-
color: var(--ps-text-muted); cursor: pointer;
|
|
17620
|
-
border-bottom: 2px solid transparent;
|
|
17621
|
-
transition: color 0.15s, border-color 0.15s;
|
|
17622
|
-
display: flex; align-items: center; justify-content: center; gap: max(6px, 1.5vw);
|
|
16579
|
+
.ps-bp-img-card-selected {
|
|
16580
|
+
border-color: var(--ps-accent) !important;
|
|
16581
|
+
border-width: 2.5px !important;
|
|
17623
16582
|
}
|
|
17624
|
-
.ps-
|
|
17625
|
-
color: var(--ps-accent);
|
|
17626
|
-
border-bottom-color: var(--ps-accent);
|
|
16583
|
+
.ps-bp-img-card-selected .ps-bp-img-card-label {
|
|
16584
|
+
font-weight: 700; color: var(--ps-accent);
|
|
17627
16585
|
}
|
|
17628
|
-
.ps-
|
|
17629
|
-
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
.ps-bp-bra-step { display: flex; flex-direction: column; justify-content: center; flex: 1; }
|
|
17634
|
-
.ps-bp-bra-step .ps-bp-title { text-align: center; margin-bottom: 1.5vw; }
|
|
17635
|
-
|
|
17636
|
-
/* Bra size selector */
|
|
17637
|
-
.ps-bp-bra-section { margin-bottom: 1.2vw; }
|
|
17638
|
-
.ps-bp-bra-header {
|
|
17639
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
17640
|
-
margin-bottom: 0.5vw;
|
|
16586
|
+
.ps-bp-img-card-check {
|
|
16587
|
+
display: none; position: absolute; top: 0.3vw; right: 0.3vw;
|
|
16588
|
+
width: 1.1vw; height: 1.1vw; border-radius: 50%;
|
|
16589
|
+
background: var(--ps-accent); align-items: center; justify-content: center;
|
|
16590
|
+
z-index: 1;
|
|
17641
16591
|
}
|
|
17642
|
-
.ps-bp-
|
|
17643
|
-
|
|
17644
|
-
|
|
16592
|
+
.ps-bp-img-card-check svg { width: 0.6vw; height: 0.6vw; }
|
|
16593
|
+
.ps-bp-img-card-selected .ps-bp-img-card-check { display: flex; }
|
|
16594
|
+
.ps-bp-img-card .ps-bp-img-card-thumb { opacity: 1; }
|
|
16595
|
+
.ps-bp-img-card-thumb {
|
|
16596
|
+
width: 100%; flex: 1; min-height: 0; overflow: hidden;
|
|
16597
|
+
display: flex; align-items: center; justify-content: center;
|
|
16598
|
+
background: transparent;
|
|
17645
16599
|
}
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
.ps-bp-photo-overlay {
|
|
17649
|
-
position: absolute;
|
|
17650
|
-
inset: 0;
|
|
17651
|
-
background: rgba(255, 255, 255, 0.92);
|
|
17652
|
-
display: flex;
|
|
17653
|
-
flex-direction: column;
|
|
17654
|
-
align-items: center;
|
|
17655
|
-
justify-content: center;
|
|
17656
|
-
gap: 0.9vw;
|
|
17657
|
-
z-index: 10;
|
|
17658
|
-
border-radius: inherit;
|
|
17659
|
-
backdrop-filter: blur(2px);
|
|
17660
|
-
animation: ps-bp-fade 0.18s ease both;
|
|
16600
|
+
.ps-bp-img-card-img {
|
|
16601
|
+
width: 100%; height: 100%; object-fit: contain;
|
|
17661
16602
|
}
|
|
17662
|
-
.ps-bp-
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17666
|
-
|
|
17667
|
-
|
|
17668
|
-
border: 2.5px solid rgba(33, 84, 239, 0.18);
|
|
17669
|
-
border-top-color: var(--ps-accent);
|
|
17670
|
-
animation: ps-bp-spin 0.7s linear infinite;
|
|
16603
|
+
.ps-bp-img-card .ps-bp-illust { width: 80%; height: 80%; }
|
|
16604
|
+
.ps-bp-img-card .ps-bp-illust svg { width: 100%; height: 100%; }
|
|
16605
|
+
.ps-bp-img-card-label {
|
|
16606
|
+
padding: 0.3vw 0.2vw; font-size: 0.6vw; font-weight: 600;
|
|
16607
|
+
color: var(--ps-text-secondary); text-align: center; width: 100%;
|
|
16608
|
+
transition: opacity 0.2s;
|
|
17671
16609
|
}
|
|
17672
|
-
|
|
17673
|
-
|
|
16610
|
+
/* Hover hint text below cards */
|
|
16611
|
+
.ps-bp-hover-hint {
|
|
16612
|
+
text-align: center; font-size: 0.8vw; color: rgba(0,0,0,0.45);
|
|
16613
|
+
margin: 0.15vw 0 0; min-height: 1em; transition: opacity 0.2s;
|
|
16614
|
+
font-weight: 400; letter-spacing: 0.01em;
|
|
17674
16615
|
}
|
|
17675
|
-
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
16616
|
+
|
|
16617
|
+
/* Photo upload */
|
|
16618
|
+
.ps-bp-photo-upload {
|
|
16619
|
+
display: flex; align-items: center; gap: 0.6vw;
|
|
16620
|
+
width: 100%; padding: 0.7vw 0.8vw;
|
|
16621
|
+
background: transparent; border: 1.5px dashed var(--ps-border-color);
|
|
16622
|
+
border-radius: 0.5vw; cursor: pointer;
|
|
16623
|
+
transition: all 0.2s; color: var(--ps-text-muted);
|
|
16624
|
+
font-family: inherit; text-align: left;
|
|
16625
|
+
}
|
|
16626
|
+
.ps-bp-photo-upload:hover {
|
|
16627
|
+
border-color: var(--ps-accent); background: rgba(33,84,239,0.04);
|
|
16628
|
+
color: var(--ps-text-secondary);
|
|
17680
16629
|
}
|
|
16630
|
+
.ps-bp-photo-upload svg { flex-shrink: 0; opacity: 0.6; }
|
|
16631
|
+
.ps-bp-photo-upload-text { font-size: 0.78vw; font-weight: 500; }
|
|
16632
|
+
.ps-bp-photo-upload-hint { font-size: 0.62vw; color: var(--ps-text-muted); margin-left: auto; }
|
|
17681
16633
|
|
|
17682
|
-
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
|
|
17686
|
-
width: min(360px, 22vw);
|
|
17687
|
-
background: #FFFFFF;
|
|
17688
|
-
border: 1px solid var(--ps-border-subtle);
|
|
17689
|
-
border-radius: 0.8vw;
|
|
17690
|
-
box-shadow: 0 20px 40px -12px rgba(17,24,39,0.25), 0 8px 16px -8px rgba(17,24,39,0.15);
|
|
17691
|
-
padding: 0.8vw 1vw;
|
|
17692
|
-
display: flex; flex-direction: column; gap: 0.6vw;
|
|
17693
|
-
z-index: 5;
|
|
16634
|
+
.ps-bp-photo-preview {
|
|
16635
|
+
display: flex; align-items: center; gap: 0.6vw;
|
|
16636
|
+
padding: 0.5vw; border: 1.5px solid var(--ps-accent);
|
|
16637
|
+
border-radius: 0.5vw; background: rgba(33,84,239,0.06);
|
|
17694
16638
|
}
|
|
17695
|
-
.ps-bp-photo-
|
|
17696
|
-
|
|
17697
|
-
|
|
17698
|
-
.ps-bp-photo-help-close {
|
|
17699
|
-
width: 1.4vw; height: 1.4vw; border-radius: 50%;
|
|
17700
|
-
background: transparent; border: none; color: var(--ps-text-muted);
|
|
17701
|
-
cursor: pointer; font-size: 1vw; line-height: 1;
|
|
16639
|
+
.ps-bp-photo-thumb {
|
|
16640
|
+
width: 2.8vw; height: 2.8vw; object-fit: cover;
|
|
16641
|
+
border-radius: 0.4vw; flex-shrink: 0;
|
|
17702
16642
|
}
|
|
17703
|
-
.ps-bp-photo-
|
|
17704
|
-
.ps-bp-photo-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
.ps-bp-photo-help-do .ps-bp-photo-help-section-label { color: #1c9d4c; }
|
|
17708
|
-
.ps-bp-photo-help-dont { background: #ffe2e2; }
|
|
17709
|
-
.ps-bp-photo-help-dont .ps-bp-photo-help-section-label { color: #e7000b; }
|
|
17710
|
-
.ps-bp-photo-help-tip {
|
|
17711
|
-
background: #c6e1f7; font-size: 0.6vw; line-height: 1.55;
|
|
17712
|
-
color: var(--ps-text-primary);
|
|
16643
|
+
.ps-bp-photo-info { display: flex; flex-direction: column; gap: 0.1vw; flex: 1; min-width: 0; }
|
|
16644
|
+
.ps-bp-photo-status {
|
|
16645
|
+
font-size: 0.73vw; font-weight: 600; color: var(--ps-text-primary);
|
|
16646
|
+
display: flex; align-items: center; gap: 0.3vw;
|
|
17713
16647
|
}
|
|
17714
|
-
.ps-bp-photo-
|
|
16648
|
+
.ps-bp-photo-hint { font-size: 0.62vw; color: var(--ps-text-muted); }
|
|
16649
|
+
.ps-bp-photo-remove {
|
|
16650
|
+
width: 1.5vw; height: 1.5vw; display: flex; align-items: center; justify-content: center;
|
|
16651
|
+
background: none; border: none; color: var(--ps-text-muted);
|
|
16652
|
+
cursor: pointer; font-size: 1vw; line-height: 1; border-radius: 0.25vw;
|
|
16653
|
+
transition: all 0.15s; flex-shrink: 0;
|
|
16654
|
+
}
|
|
16655
|
+
.ps-bp-photo-remove:hover { background: rgba(0,0,0,0.06); color: var(--ps-text-primary); }
|
|
17715
16656
|
|
|
17716
|
-
/*
|
|
17717
|
-
.ps-bp-
|
|
17718
|
-
|
|
17719
|
-
display: inline-flex;
|
|
17720
|
-
width: 100%;
|
|
16657
|
+
/* Error */
|
|
16658
|
+
.ps-bp-error {
|
|
16659
|
+
color: var(--ps-error-color); font-size: 0.73vw; margin: 0;
|
|
17721
16660
|
}
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
16661
|
+
|
|
16662
|
+
/* Navigation — back left, next right */
|
|
16663
|
+
.ps-bp-nav {
|
|
16664
|
+
display: flex; align-items: center; margin-top: auto;
|
|
17725
16665
|
justify-content: space-between;
|
|
17726
|
-
gap: 0.4vw;
|
|
17727
|
-
width: 100%;
|
|
17728
|
-
min-width: 4vw;
|
|
17729
|
-
padding: 0.45vw 0.7vw;
|
|
17730
|
-
background: var(--ps-bg-secondary, #fff);
|
|
17731
|
-
border: 1.5px solid rgba(0, 0, 0, 0.12);
|
|
17732
|
-
border-radius: 0.4vw;
|
|
17733
|
-
color: var(--ps-text-primary);
|
|
17734
|
-
font-size: max(11px, 0.78vw);
|
|
17735
|
-
font-weight: 600;
|
|
17736
|
-
font-family: inherit;
|
|
17737
|
-
cursor: pointer;
|
|
17738
|
-
transition: border-color 0.18s, background 0.18s, transform 0.12s;
|
|
17739
16666
|
}
|
|
17740
|
-
.ps-bp-
|
|
17741
|
-
border
|
|
16667
|
+
.ps-bp-back-btn {
|
|
16668
|
+
background: none; border: none; padding: 0;
|
|
16669
|
+
color: var(--ps-text-muted); cursor: pointer;
|
|
16670
|
+
font-size: 0.83vw; font-weight: 500; font-family: inherit;
|
|
16671
|
+
white-space: nowrap; display: flex; align-items: center; gap: 0.3vw;
|
|
16672
|
+
transition: color 0.2s;
|
|
17742
16673
|
}
|
|
17743
|
-
.ps-bp-
|
|
17744
|
-
|
|
17745
|
-
|
|
16674
|
+
.ps-bp-back-btn:hover { color: var(--ps-text-primary); }
|
|
16675
|
+
.ps-bp-back-arrow {
|
|
16676
|
+
display: inline-block; font-size: 1.1vw; transition: transform 0.25s ease;
|
|
17746
16677
|
}
|
|
17747
|
-
.ps-bp-
|
|
17748
|
-
.ps-bp-
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
16678
|
+
.ps-bp-back-btn:hover .ps-bp-back-arrow { transform: translateX(-4px); }
|
|
16679
|
+
.ps-bp-next-btn {
|
|
16680
|
+
display: flex; align-items: center; gap: 0.5vw;
|
|
16681
|
+
padding: 0.45vw 1.2vw;
|
|
16682
|
+
background: var(--ps-accent); border: none; border-radius: 0.35vw;
|
|
16683
|
+
color: #fff; font-size: 0.73vw; font-weight: 700;
|
|
16684
|
+
cursor: pointer; transition: all 0.15s; font-family: inherit;
|
|
16685
|
+
white-space: nowrap;
|
|
17752
16686
|
}
|
|
17753
|
-
.ps-bp-
|
|
17754
|
-
|
|
17755
|
-
|
|
17756
|
-
|
|
16687
|
+
.ps-bp-next-btn svg { width: 0.75vw; height: 0.75vw; }
|
|
16688
|
+
.ps-bp-next-btn:hover { background: var(--ps-accent-hover); }
|
|
16689
|
+
.ps-bp-btn-disabled {
|
|
16690
|
+
opacity: 0.35; cursor: not-allowed; pointer-events: none;
|
|
17757
16691
|
}
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
|
|
16692
|
+
|
|
16693
|
+
/* ── Mobile responsive ── */
|
|
16694
|
+
@media (max-width: 768px) {
|
|
16695
|
+
.ps-bp-layout { flex-direction: column; height: auto; gap: 0; overflow: visible; }
|
|
16696
|
+
/* Desktop layout's product image — hidden on mobile because BasicsStepMobile
|
|
16697
|
+
has its own minimal layout. The other steps (chest/midsection/hips) don't
|
|
16698
|
+
need the giant left-column image on a phone either. */
|
|
16699
|
+
.ps-bp-image { display: none !important; }
|
|
16700
|
+
.ps-bp-wrapper { overflow-y: auto; -webkit-overflow-scrolling: touch; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
16701
|
+
.ps-bp-layout { flex: 1; min-height: 0; }
|
|
16702
|
+
/* Override desktop justify-content:center so BasicsStepMobile can
|
|
16703
|
+
stretch to the bottom of the modal — its bottom action bar (Next +
|
|
16704
|
+
tabs) needs to land at the bottom edge, not floating in the middle. */
|
|
16705
|
+
.ps-bp-root {
|
|
16706
|
+
justify-content: stretch !important;
|
|
16707
|
+
height: auto !important;
|
|
16708
|
+
max-height: none !important;
|
|
16709
|
+
overflow: visible !important;
|
|
16710
|
+
flex: 1 !important;
|
|
16711
|
+
}
|
|
16712
|
+
.ps-bp-upload-placeholder svg { width: 32px; height: 32px; }
|
|
16713
|
+
.ps-bp-upload-text { font-size: 14px; }
|
|
16714
|
+
.ps-bp-upload-hint { font-size: 11px; }
|
|
16715
|
+
.ps-bp-root { padding: 16px; gap: 14px; overflow: visible; }
|
|
16716
|
+
.ps-bp-dots { gap: 6px; }
|
|
16717
|
+
.ps-bp-dot { width: 6px; height: 6px; }
|
|
16718
|
+
.ps-bp-dot-active { width: 18px; border-radius: 4px; }
|
|
16719
|
+
.ps-bp-title { font-size: 18px; }
|
|
16720
|
+
.ps-bp-subtitle { font-size: 13px; margin-top: -4px; }
|
|
16721
|
+
.ps-bp-system-toggle { gap: 20px; margin: 4px 0; }
|
|
16722
|
+
.ps-bp-system-btn { font-size: 13px; }
|
|
16723
|
+
.ps-bp-inline-row { padding: 14px 0; gap: 12px; }
|
|
16724
|
+
.ps-bp-inline-label { flex: 0 0 85px; font-size: 11px; white-space: nowrap; }
|
|
16725
|
+
.ps-bp-inline-input { font-size: 18px; }
|
|
16726
|
+
.ps-bp-inline-unit { font-size: 12px; }
|
|
16727
|
+
.ps-bp-photo-cta { font-size: 13px; gap: 6px; margin-top: 4px; }
|
|
16728
|
+
.ps-bp-photo-cta svg { width: 16px; height: 16px; }
|
|
16729
|
+
.ps-bp-options { gap: 6px; }
|
|
16730
|
+
.ps-bp-option { border-radius: 10px; padding: 6px 12px 6px 6px; gap: 10px; }
|
|
16731
|
+
.ps-bp-option-icon { width: 70px; height: 70px; }
|
|
16732
|
+
.ps-bp-option-label { font-size: 14px; }
|
|
16733
|
+
.ps-bp-option::after { font-size: 18px; }
|
|
16734
|
+
.ps-bp-option { padding: 14px 8px; border-radius: 10px; gap: 8px; }
|
|
16735
|
+
.ps-bp-option-icon { width: 56px; height: 56px; }
|
|
16736
|
+
.ps-bp-option-label { font-size: 13px; }
|
|
16737
|
+
.ps-bp-error { font-size: 12px; }
|
|
16738
|
+
.ps-bp-shape-section { margin-bottom: 12px; }
|
|
16739
|
+
.ps-bp-shape-label { font-size: 13px; margin-bottom: 6px; }
|
|
16740
|
+
.ps-bp-shape-row { gap: 6px; }
|
|
16741
|
+
.ps-bp-img-card { border-radius: 8px; border-width: 2px; }
|
|
16742
|
+
.ps-bp-img-card-label { font-size: 11px; padding: 4px 2px; }
|
|
16743
|
+
.ps-bp-hover-hint { font-size: 10px; margin-top: 6px; }
|
|
16744
|
+
.ps-bp-single-q .ps-bp-title { font-size: 13px; font-weight: 400; }
|
|
16745
|
+
.ps-bp-photo-upload { padding: 10px 12px; gap: 8px; border-radius: 8px; }
|
|
16746
|
+
.ps-bp-photo-upload-text { font-size: 13px; }
|
|
16747
|
+
.ps-bp-photo-upload-hint { font-size: 10px; }
|
|
16748
|
+
.ps-bp-photo-preview { padding: 8px; gap: 8px; border-radius: 8px; }
|
|
16749
|
+
.ps-bp-photo-thumb { width: 40px; height: 40px; border-radius: 6px; }
|
|
16750
|
+
.ps-bp-photo-status { font-size: 12px; gap: 4px; }
|
|
16751
|
+
.ps-bp-photo-hint { font-size: 10px; }
|
|
16752
|
+
.ps-bp-photo-remove { width: 22px; height: 22px; font-size: 16px; }
|
|
16753
|
+
.ps-bp-nav { padding-top: 6px; }
|
|
16754
|
+
.ps-bp-back-btn { font-size: 14px; }
|
|
16755
|
+
.ps-bp-back-arrow { font-size: 18px; }
|
|
16756
|
+
.ps-bp-next-btn {
|
|
16757
|
+
display: flex; align-items: center; gap: 0.5vw; padding: 10px 16px; font-size: 13px; border-radius: 6px; }
|
|
16758
|
+
.ps-bp-root { padding: 12px; overflow: hidden; }
|
|
16759
|
+
/* Accessory sizing inputs — mobile */
|
|
16760
|
+
.ps-acc-input { padding: 3vw; font-size: 3.6vw; border-radius: 2vw; }
|
|
16761
|
+
.ps-acc-label { font-size: 2.8vw; margin-bottom: 1.5vw; }
|
|
16762
|
+
.ps-acc-field { margin: 3vw 0; }
|
|
16763
|
+
.ps-acc-error { font-size: 2.8vw; }
|
|
16764
|
+
.ps-acc-hint { font-size: 2.5vw; }
|
|
16765
|
+
.ps-tryon-unit-toggle { border-radius: 2vw; }
|
|
16766
|
+
.ps-tryon-unit-btn { padding: 2vw 4vw; font-size: 3vw; }
|
|
17761
16767
|
}
|
|
17762
|
-
|
|
17763
|
-
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
border
|
|
17771
|
-
|
|
17772
|
-
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
z-index: 250;
|
|
16768
|
+
|
|
16769
|
+
/* Upload hover overlay */
|
|
16770
|
+
.ps-tryon-upload-hover:hover .ps-tryon-upload-hover-overlay { opacity: 1 !important; }
|
|
16771
|
+
|
|
16772
|
+
/* ─────────── Unified PhotoUploadZone ─────────── */
|
|
16773
|
+
.ps-photo-zone {
|
|
16774
|
+
flex: 1; min-height: 220px;
|
|
16775
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
16776
|
+
border: 2px dashed var(--ps-border-color);
|
|
16777
|
+
border-radius: 0.5vw;
|
|
16778
|
+
background: var(--ps-bg-secondary);
|
|
16779
|
+
cursor: pointer; position: relative; overflow: hidden;
|
|
16780
|
+
transition: border-color 0.18s, background 0.18s, transform 0.18s;
|
|
17776
16781
|
}
|
|
17777
|
-
.ps-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
16782
|
+
.ps-photo-zone:hover { border-color: var(--ps-accent); background: rgba(33,84,239,0.02); }
|
|
16783
|
+
.ps-photo-zone.ps-photo-zone-drag {
|
|
16784
|
+
border-color: var(--ps-accent); border-style: solid;
|
|
16785
|
+
background: rgba(33,84,239,0.06);
|
|
16786
|
+
transform: scale(1.005);
|
|
17781
16787
|
}
|
|
17782
|
-
.ps-
|
|
17783
|
-
|
|
17784
|
-
|
|
17785
|
-
|
|
16788
|
+
.ps-photo-zone.ps-photo-zone-has { border: none; cursor: default; padding: 0; }
|
|
16789
|
+
.ps-photo-zone.ps-photo-zone-inline { min-height: 100%; height: 100%; }
|
|
16790
|
+
|
|
16791
|
+
.ps-photo-zone-empty {
|
|
16792
|
+
display: flex; flex-direction: column; align-items: center; gap: 0.4vw;
|
|
16793
|
+
padding: 1vw; text-align: center; pointer-events: none;
|
|
17786
16794
|
}
|
|
17787
|
-
|
|
17788
|
-
|
|
17789
|
-
|
|
16795
|
+
.ps-photo-zone-title { font-size: 0.85vw; font-weight: 600; color: var(--ps-text-primary); }
|
|
16796
|
+
.ps-photo-zone-hint { font-size: 0.6vw; color: var(--ps-text-muted); line-height: 1.4; max-width: 24vw; }
|
|
16797
|
+
|
|
16798
|
+
.ps-photo-zone-img {
|
|
16799
|
+
width: 100%; height: 100%; object-fit: contain;
|
|
16800
|
+
display: block; cursor: pointer;
|
|
17790
16801
|
}
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
16802
|
+
.ps-photo-zone-hover-overlay {
|
|
16803
|
+
position: absolute; inset: 0;
|
|
16804
|
+
display: flex; align-items: center; justify-content: center;
|
|
16805
|
+
background: rgba(0,0,0,0.4); opacity: 0; transition: opacity 0.2s;
|
|
16806
|
+
color: #fff; font-size: 0.8vw; font-weight: 600;
|
|
16807
|
+
border-radius: 0.5vw; cursor: pointer;
|
|
17794
16808
|
}
|
|
17795
|
-
.ps-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
|
|
17802
|
-
|
|
17803
|
-
font-size: max(11px, 0.78vw);
|
|
17804
|
-
font-weight: 600;
|
|
17805
|
-
font-family: inherit;
|
|
17806
|
-
text-align: left;
|
|
17807
|
-
cursor: pointer;
|
|
17808
|
-
transition: background 0.12s, color 0.12s, transform 0.12s;
|
|
16809
|
+
.ps-photo-zone:hover .ps-photo-zone-hover-overlay { opacity: 1; }
|
|
16810
|
+
.ps-photo-zone-remove {
|
|
16811
|
+
position: absolute; top: 0.5vw; right: 0.5vw;
|
|
16812
|
+
background: rgba(0,0,0,0.55); color: #fff; border: none; border-radius: 50%;
|
|
16813
|
+
width: 24px; height: 24px; min-width: 24px;
|
|
16814
|
+
display: flex; align-items: center; justify-content: center;
|
|
16815
|
+
cursor: pointer; transition: background 0.15s;
|
|
16816
|
+
z-index: 2;
|
|
17809
16817
|
}
|
|
17810
|
-
.ps-
|
|
17811
|
-
|
|
17812
|
-
|
|
16818
|
+
.ps-photo-zone-remove:hover { background: rgba(239,68,68,0.85); }
|
|
16819
|
+
|
|
16820
|
+
.ps-photo-zone-processing {
|
|
16821
|
+
display: flex; flex-direction: column; align-items: center; gap: 0.5vw;
|
|
16822
|
+
padding: 1vw;
|
|
17813
16823
|
}
|
|
17814
|
-
.ps-
|
|
17815
|
-
|
|
17816
|
-
|
|
16824
|
+
.ps-photo-zone-spinner {
|
|
16825
|
+
width: 28px; height: 28px;
|
|
16826
|
+
border: 3px solid rgba(33,84,239,0.18);
|
|
16827
|
+
border-top-color: var(--ps-accent);
|
|
16828
|
+
border-radius: 50%;
|
|
16829
|
+
animation: ps-spin 0.7s linear infinite;
|
|
17817
16830
|
}
|
|
16831
|
+
.ps-photo-zone-status { font-size: 0.7vw; color: var(--ps-text-secondary); }
|
|
17818
16832
|
|
|
17819
|
-
.ps-
|
|
17820
|
-
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
border-radius: 0.4vw; background: transparent;
|
|
17824
|
-
color: var(--ps-text-secondary); font-size: 0.8vw; font-weight: 600;
|
|
17825
|
-
cursor: pointer; font-family: inherit; transition: all 0.25s;
|
|
16833
|
+
.ps-photo-zone-rejection {
|
|
16834
|
+
display: flex; flex-direction: column; align-items: center; gap: 0.5vw;
|
|
16835
|
+
padding: 1vw 1.2vw; max-width: 22vw; text-align: center;
|
|
16836
|
+
cursor: default;
|
|
17826
16837
|
}
|
|
17827
|
-
.ps-
|
|
17828
|
-
|
|
17829
|
-
|
|
16838
|
+
.ps-photo-zone-rejection-icon {
|
|
16839
|
+
width: 36px; height: 36px; border-radius: 50%;
|
|
16840
|
+
background: rgba(239,68,68,0.12); color: #dc2626;
|
|
16841
|
+
display: flex; align-items: center; justify-content: center;
|
|
16842
|
+
font-size: 18px; font-weight: 700;
|
|
17830
16843
|
}
|
|
17831
|
-
.ps-
|
|
17832
|
-
.ps-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
16844
|
+
.ps-photo-zone-rejection-title { font-size: 0.85vw; font-weight: 700; color: var(--ps-text-primary); }
|
|
16845
|
+
.ps-photo-zone-rejection-msg { font-size: 0.65vw; color: var(--ps-text-secondary); line-height: 1.5; }
|
|
16846
|
+
.ps-photo-zone-rejection-cta {
|
|
16847
|
+
margin-top: 0.3vw; padding: 0.55vw 1vw;
|
|
16848
|
+
background: var(--ps-accent); color: #fff; border: none;
|
|
16849
|
+
border-radius: 0.4vw; font-family: inherit;
|
|
16850
|
+
font-size: 0.7vw; font-weight: 600; cursor: pointer;
|
|
16851
|
+
transition: opacity 0.15s;
|
|
17839
16852
|
}
|
|
17840
|
-
.ps-
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
16853
|
+
.ps-photo-zone-rejection-cta:hover { opacity: 0.9; }
|
|
16854
|
+
|
|
16855
|
+
.ps-photo-zone-error {
|
|
16856
|
+
position: absolute; bottom: 0.6vw; left: 0.6vw; right: 0.6vw;
|
|
16857
|
+
background: rgba(239,68,68,0.08); color: #dc2626;
|
|
16858
|
+
border: 1px solid rgba(239,68,68,0.2); border-radius: 0.4vw;
|
|
16859
|
+
padding: 0.4vw 0.6vw; font-size: 0.6vw; line-height: 1.4;
|
|
16860
|
+
text-align: center; pointer-events: none;
|
|
17846
16861
|
}
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
16862
|
+
|
|
16863
|
+
@media (max-width: 700px) {
|
|
16864
|
+
.ps-photo-zone { min-height: 200px; border-radius: 12px; }
|
|
16865
|
+
.ps-photo-zone-title { font-size: 14px; }
|
|
16866
|
+
.ps-photo-zone-hint { font-size: 11px; max-width: 90%; }
|
|
16867
|
+
.ps-photo-zone-hover-overlay { font-size: 13px; }
|
|
16868
|
+
.ps-photo-zone-status { font-size: 12px; }
|
|
16869
|
+
.ps-photo-zone-rejection { max-width: 90%; gap: 8px; padding: 14px; }
|
|
16870
|
+
.ps-photo-zone-rejection-title { font-size: 14px; }
|
|
16871
|
+
.ps-photo-zone-rejection-msg { font-size: 12px; }
|
|
16872
|
+
.ps-photo-zone-rejection-cta { font-size: 13px; padding: 10px 16px; border-radius: 8px; }
|
|
16873
|
+
.ps-photo-zone-error { font-size: 11px; padding: 8px 10px; border-radius: 8px; }
|
|
17851
16874
|
}
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
|
|
17855
|
-
|
|
17856
|
-
|
|
17857
|
-
|
|
16875
|
+
`;
|
|
16876
|
+
var jsxRuntime = { exports: {} };
|
|
16877
|
+
var reactJsxRuntime_production_min = {};
|
|
16878
|
+
/**
|
|
16879
|
+
* @license React
|
|
16880
|
+
* react-jsx-runtime.production.min.js
|
|
16881
|
+
*
|
|
16882
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
16883
|
+
*
|
|
16884
|
+
* This source code is licensed under the MIT license found in the
|
|
16885
|
+
* LICENSE file in the root directory of this source tree.
|
|
16886
|
+
*/
|
|
16887
|
+
var f = reactExports, k = Symbol.for("react.element"), l = Symbol.for("react.fragment"), m = Object.prototype.hasOwnProperty, n = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner, p = { key: true, ref: true, __self: true, __source: true };
|
|
16888
|
+
function q(c, a, g) {
|
|
16889
|
+
var b, d = {}, e = null, h = null;
|
|
16890
|
+
void 0 !== g && (e = "" + g);
|
|
16891
|
+
void 0 !== a.key && (e = "" + a.key);
|
|
16892
|
+
void 0 !== a.ref && (h = a.ref);
|
|
16893
|
+
for (b in a) m.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
|
|
16894
|
+
if (c && c.defaultProps) for (b in a = c.defaultProps, a) void 0 === d[b] && (d[b] = a[b]);
|
|
16895
|
+
return { $$typeof: k, type: c, key: e, ref: h, props: d, _owner: n.current };
|
|
16896
|
+
}
|
|
16897
|
+
reactJsxRuntime_production_min.Fragment = l;
|
|
16898
|
+
reactJsxRuntime_production_min.jsx = q;
|
|
16899
|
+
reactJsxRuntime_production_min.jsxs = q;
|
|
16900
|
+
{
|
|
16901
|
+
jsxRuntime.exports = reactJsxRuntime_production_min;
|
|
16902
|
+
}
|
|
16903
|
+
var jsxRuntimeExports = jsxRuntime.exports;
|
|
16904
|
+
const STORAGE_KEY = "ps_session";
|
|
16905
|
+
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
16906
|
+
const FALLBACK_PREFIX = "ps_mem_";
|
|
16907
|
+
let memoryFallback = null;
|
|
16908
|
+
function hasLocalStorage() {
|
|
16909
|
+
try {
|
|
16910
|
+
return typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
16911
|
+
} catch {
|
|
16912
|
+
return false;
|
|
17858
16913
|
}
|
|
17859
|
-
|
|
17860
|
-
|
|
17861
|
-
|
|
17862
|
-
|
|
16914
|
+
}
|
|
16915
|
+
function uuid() {
|
|
16916
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
16917
|
+
return crypto.randomUUID();
|
|
17863
16918
|
}
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
16919
|
+
const chars = "0123456789abcdef";
|
|
16920
|
+
let out = FALLBACK_PREFIX;
|
|
16921
|
+
for (let i = 0; i < 32; i++) out += chars[Math.floor(Math.random() * 16)];
|
|
16922
|
+
return out;
|
|
16923
|
+
}
|
|
16924
|
+
function readRecord() {
|
|
16925
|
+
if (!hasLocalStorage()) return memoryFallback;
|
|
16926
|
+
try {
|
|
16927
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
16928
|
+
if (!raw) return null;
|
|
16929
|
+
const parsed = JSON.parse(raw);
|
|
16930
|
+
if (typeof parsed.id !== "string" || typeof parsed.issuedAt !== "number") return null;
|
|
16931
|
+
return {
|
|
16932
|
+
id: parsed.id,
|
|
16933
|
+
issuedAt: parsed.issuedAt,
|
|
16934
|
+
lastSeenAt: typeof parsed.lastSeenAt === "number" ? parsed.lastSeenAt : parsed.issuedAt
|
|
16935
|
+
};
|
|
16936
|
+
} catch {
|
|
16937
|
+
return null;
|
|
17868
16938
|
}
|
|
17869
|
-
|
|
17870
|
-
|
|
16939
|
+
}
|
|
16940
|
+
function writeRecord(record) {
|
|
16941
|
+
memoryFallback = record;
|
|
16942
|
+
if (!hasLocalStorage()) return;
|
|
16943
|
+
try {
|
|
16944
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(record));
|
|
16945
|
+
} catch {
|
|
17871
16946
|
}
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
.
|
|
17875
|
-
|
|
17876
|
-
|
|
16947
|
+
}
|
|
16948
|
+
function getOrCreateSessionId() {
|
|
16949
|
+
const now = Date.now();
|
|
16950
|
+
const existing = readRecord();
|
|
16951
|
+
if (existing && now - existing.lastSeenAt < MAX_AGE_MS) {
|
|
16952
|
+
if (now - existing.lastSeenAt > 5 * 60 * 1e3) {
|
|
16953
|
+
writeRecord({ ...existing, lastSeenAt: now });
|
|
16954
|
+
}
|
|
16955
|
+
return existing.id;
|
|
17877
16956
|
}
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
|
|
16957
|
+
const fresh = { id: uuid(), issuedAt: now, lastSeenAt: now };
|
|
16958
|
+
writeRecord(fresh);
|
|
16959
|
+
return fresh.id;
|
|
16960
|
+
}
|
|
16961
|
+
function getDeviceHint() {
|
|
16962
|
+
if (typeof navigator === "undefined") return null;
|
|
16963
|
+
const ua2 = (navigator.userAgent || "").toLowerCase();
|
|
16964
|
+
if (/ipad|tablet|(android(?!.*mobile))/.test(ua2)) return "tablet";
|
|
16965
|
+
if (/mobile|android|iphone|ipod|blackberry|iemobile|opera mini/.test(ua2)) return "mobile";
|
|
16966
|
+
return "desktop";
|
|
16967
|
+
}
|
|
16968
|
+
const DEFAULT_API_URL = "http://localhost:4000";
|
|
16969
|
+
class ApiClient {
|
|
16970
|
+
constructor(apiKey, apiUrl) {
|
|
16971
|
+
this.apiKey = apiKey;
|
|
16972
|
+
this.baseUrl = (apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
17885
16973
|
}
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
16974
|
+
get headers() {
|
|
16975
|
+
return {
|
|
16976
|
+
"Content-Type": "application/json",
|
|
16977
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
16978
|
+
};
|
|
17890
16979
|
}
|
|
17891
|
-
|
|
17892
|
-
|
|
17893
|
-
|
|
17894
|
-
|
|
16980
|
+
async submitTryOn(modelImage, garmentImage, fitInfo, category, context) {
|
|
16981
|
+
const body = {
|
|
16982
|
+
garmentImage,
|
|
16983
|
+
// Attribution fields — backend logs these into the TryOnEvent collection
|
|
16984
|
+
// so non-technical operators can answer "which products are being tried on
|
|
16985
|
+
// and by how many customers". Safe fallbacks when absent.
|
|
16986
|
+
sessionId: getOrCreateSessionId(),
|
|
16987
|
+
deviceHint: getDeviceHint()
|
|
16988
|
+
};
|
|
16989
|
+
if (context?.modelImageId) body.modelImageId = context.modelImageId;
|
|
16990
|
+
if (modelImage) body.modelImage = modelImage;
|
|
16991
|
+
if (fitInfo && fitInfo.length > 0) body.fitInfo = fitInfo;
|
|
16992
|
+
if (category && category !== "apparel") body.category = category;
|
|
16993
|
+
if (context?.productId) body.productId = context.productId;
|
|
16994
|
+
if (context?.productTitle) body.productTitle = context.productTitle;
|
|
16995
|
+
if (context?.productDescription) body.productDescription = context.productDescription;
|
|
16996
|
+
if (context?.productMaterial) body.productMaterial = context.productMaterial;
|
|
16997
|
+
if (context?.silhouetteContext) body.silhouetteContext = context.silhouetteContext;
|
|
16998
|
+
if (context?.editFromPrevious) body.editFromPrevious = true;
|
|
16999
|
+
console.log("[ps-sdk:api] POST /api/v1/tryon payload", {
|
|
17000
|
+
modelImageBytes: modelImage.length,
|
|
17001
|
+
garmentImageBytes: garmentImage.length,
|
|
17002
|
+
category: category || "apparel",
|
|
17003
|
+
fitInfo: fitInfo?.map((f2) => ({
|
|
17004
|
+
area: f2.area,
|
|
17005
|
+
fit: f2.fit,
|
|
17006
|
+
userValue: f2.userValue,
|
|
17007
|
+
garmentRange: f2.garmentRange
|
|
17008
|
+
})) || null,
|
|
17009
|
+
silhouetteContext: context?.silhouetteContext || null
|
|
17010
|
+
});
|
|
17011
|
+
if (fitInfo && fitInfo.length > 0) {
|
|
17012
|
+
console.log("[ps-sdk:api] Body vs Garment (what Gemini will see):");
|
|
17013
|
+
for (const f2 of fitInfo) {
|
|
17014
|
+
const parts = (f2.garmentRange || "").replace(/[^\d.\-–]/g, " ").trim().split(/[\s\-–]+/).filter(Boolean).map(Number).filter((n2) => !isNaN(n2));
|
|
17015
|
+
let gap = "";
|
|
17016
|
+
if (parts.length && f2.userValue != null) {
|
|
17017
|
+
const mid = (Math.min(...parts) + Math.max(...parts)) / 2;
|
|
17018
|
+
const diff = Math.round((f2.userValue - mid) * 10) / 10;
|
|
17019
|
+
gap = diff > 0 ? `garment ${diff} smaller` : diff < 0 ? `garment ${Math.abs(diff)} larger` : "matched";
|
|
17020
|
+
}
|
|
17021
|
+
console.log(`[ps-sdk:api] ${f2.area.padEnd(10)} body=${f2.userValue ?? "?"} garment=${f2.garmentRange ?? "?"} → ${gap}`);
|
|
17022
|
+
}
|
|
17023
|
+
}
|
|
17024
|
+
const res = await fetch(`${this.baseUrl}/api/v1/tryon`, {
|
|
17025
|
+
method: "POST",
|
|
17026
|
+
headers: this.headers,
|
|
17027
|
+
body: JSON.stringify(body)
|
|
17028
|
+
});
|
|
17029
|
+
if (!res.ok) {
|
|
17030
|
+
const data = await res.json().catch(() => ({}));
|
|
17031
|
+
if (res.status === 402) {
|
|
17032
|
+
throw new PrimeStyleError(
|
|
17033
|
+
data.message || "Insufficient try-ons",
|
|
17034
|
+
"INSUFFICIENT_BALANCE"
|
|
17035
|
+
);
|
|
17036
|
+
}
|
|
17037
|
+
throw new PrimeStyleError(
|
|
17038
|
+
data.message || "Failed to submit try-on",
|
|
17039
|
+
"API_ERROR"
|
|
17040
|
+
);
|
|
17041
|
+
}
|
|
17042
|
+
return res.json();
|
|
17895
17043
|
}
|
|
17896
|
-
|
|
17897
|
-
|
|
17898
|
-
|
|
17899
|
-
|
|
17044
|
+
async getStatus(jobId) {
|
|
17045
|
+
const res = await fetch(`${this.baseUrl}/api/v1/tryon/status/${jobId}`, {
|
|
17046
|
+
headers: this.headers
|
|
17047
|
+
});
|
|
17048
|
+
if (!res.ok) {
|
|
17049
|
+
const data = await res.json().catch(() => ({}));
|
|
17050
|
+
throw new PrimeStyleError(
|
|
17051
|
+
data.message || "Failed to get status",
|
|
17052
|
+
"API_ERROR"
|
|
17053
|
+
);
|
|
17054
|
+
}
|
|
17055
|
+
return res.json();
|
|
17900
17056
|
}
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
.ps-bp-option-label {
|
|
17904
|
-
flex: 1; font-size: 0.73vw; font-weight: 500;
|
|
17905
|
-
color: #555; text-align: center;
|
|
17057
|
+
getStreamUrl() {
|
|
17058
|
+
return `${this.baseUrl}/api/v1/tryon/stream?key=${encodeURIComponent(this.apiKey)}`;
|
|
17906
17059
|
}
|
|
17907
|
-
|
|
17908
|
-
|
|
17909
|
-
|
|
17910
|
-
|
|
17060
|
+
}
|
|
17061
|
+
class PrimeStyleError extends Error {
|
|
17062
|
+
constructor(message, code) {
|
|
17063
|
+
super(message);
|
|
17064
|
+
this.name = "PrimeStyleError";
|
|
17065
|
+
this.code = code;
|
|
17911
17066
|
}
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
|
|
17916
|
-
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
17921
|
-
|
|
17922
|
-
/* Body shapes screen — fit all questions without scrolling */
|
|
17923
|
-
.ps-bp-shapes-fit { display: flex; flex-direction: column; flex: 1; overflow: hidden; gap: 0.4vw; justify-content: flex-start !important; }
|
|
17924
|
-
|
|
17925
|
-
/* Body shapes combined screen */
|
|
17926
|
-
.ps-bp-shape-section {
|
|
17927
|
-
margin-bottom: 0.4vw; flex: 1; display: flex; flex-direction: column; min-height: 0;
|
|
17067
|
+
}
|
|
17068
|
+
class SseClient {
|
|
17069
|
+
constructor(streamUrl) {
|
|
17070
|
+
this.eventSource = null;
|
|
17071
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
17072
|
+
this.reconnectTimer = null;
|
|
17073
|
+
this.reconnectAttempts = 0;
|
|
17074
|
+
this.maxReconnectAttempts = 5;
|
|
17075
|
+
this.streamUrl = streamUrl;
|
|
17928
17076
|
}
|
|
17929
|
-
|
|
17930
|
-
|
|
17931
|
-
|
|
17932
|
-
|
|
17077
|
+
connect() {
|
|
17078
|
+
if (this.eventSource) return;
|
|
17079
|
+
this.eventSource = new EventSource(this.streamUrl);
|
|
17080
|
+
this.eventSource.addEventListener("vto-update", (event) => {
|
|
17081
|
+
try {
|
|
17082
|
+
const data = JSON.parse(event.data);
|
|
17083
|
+
this.emit(data.galleryId, data);
|
|
17084
|
+
} catch {
|
|
17085
|
+
}
|
|
17086
|
+
});
|
|
17087
|
+
this.eventSource.onopen = () => {
|
|
17088
|
+
this.reconnectAttempts = 0;
|
|
17089
|
+
};
|
|
17090
|
+
this.eventSource.onerror = () => {
|
|
17091
|
+
this.eventSource?.close();
|
|
17092
|
+
this.eventSource = null;
|
|
17093
|
+
this.scheduleReconnect();
|
|
17094
|
+
};
|
|
17933
17095
|
}
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17096
|
+
scheduleReconnect() {
|
|
17097
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
|
|
17098
|
+
if (this.listeners.size === 0) return;
|
|
17099
|
+
const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 3e4);
|
|
17100
|
+
this.reconnectAttempts++;
|
|
17101
|
+
this.reconnectTimer = setTimeout(() => {
|
|
17102
|
+
this.connect();
|
|
17103
|
+
}, delay);
|
|
17937
17104
|
}
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17105
|
+
onJob(jobId, callback) {
|
|
17106
|
+
if (!this.listeners.has(jobId)) {
|
|
17107
|
+
this.listeners.set(jobId, /* @__PURE__ */ new Set());
|
|
17108
|
+
}
|
|
17109
|
+
this.listeners.get(jobId).add(callback);
|
|
17110
|
+
if (!this.eventSource) {
|
|
17111
|
+
this.connect();
|
|
17112
|
+
}
|
|
17113
|
+
return () => {
|
|
17114
|
+
const jobListeners = this.listeners.get(jobId);
|
|
17115
|
+
if (jobListeners) {
|
|
17116
|
+
jobListeners.delete(callback);
|
|
17117
|
+
if (jobListeners.size === 0) {
|
|
17118
|
+
this.listeners.delete(jobId);
|
|
17119
|
+
}
|
|
17120
|
+
}
|
|
17121
|
+
if (this.listeners.size === 0) {
|
|
17122
|
+
this.disconnect();
|
|
17123
|
+
}
|
|
17124
|
+
};
|
|
17941
17125
|
}
|
|
17942
|
-
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
position: relative;
|
|
17126
|
+
emit(jobId, update) {
|
|
17127
|
+
const callbacks = this.listeners.get(jobId);
|
|
17128
|
+
if (callbacks) {
|
|
17129
|
+
callbacks.forEach((cb2) => cb2(update));
|
|
17130
|
+
}
|
|
17948
17131
|
}
|
|
17949
|
-
|
|
17950
|
-
|
|
17951
|
-
|
|
17132
|
+
disconnect() {
|
|
17133
|
+
if (this.reconnectTimer) {
|
|
17134
|
+
clearTimeout(this.reconnectTimer);
|
|
17135
|
+
this.reconnectTimer = null;
|
|
17136
|
+
}
|
|
17137
|
+
if (this.eventSource) {
|
|
17138
|
+
this.eventSource.close();
|
|
17139
|
+
this.eventSource = null;
|
|
17140
|
+
}
|
|
17141
|
+
this.listeners.clear();
|
|
17142
|
+
this.reconnectAttempts = 0;
|
|
17952
17143
|
}
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17144
|
+
}
|
|
17145
|
+
const DEFAULT_MAX_DIMENSION = 512;
|
|
17146
|
+
const DEFAULT_JPEG_QUALITY = 0.65;
|
|
17147
|
+
function compressImage(file, opts = {}) {
|
|
17148
|
+
const maxDim = opts.maxDimension ?? DEFAULT_MAX_DIMENSION;
|
|
17149
|
+
const quality = opts.quality ?? DEFAULT_JPEG_QUALITY;
|
|
17150
|
+
return new Promise((resolve, reject) => {
|
|
17151
|
+
const reader = new FileReader();
|
|
17152
|
+
reader.onload = () => {
|
|
17153
|
+
const img = new Image();
|
|
17154
|
+
img.onload = () => {
|
|
17155
|
+
try {
|
|
17156
|
+
const canvas = document.createElement("canvas");
|
|
17157
|
+
let { width, height } = img;
|
|
17158
|
+
if (width > maxDim || height > maxDim) {
|
|
17159
|
+
if (width > height) {
|
|
17160
|
+
height = Math.round(height * maxDim / width);
|
|
17161
|
+
width = maxDim;
|
|
17162
|
+
} else {
|
|
17163
|
+
width = Math.round(width * maxDim / height);
|
|
17164
|
+
height = maxDim;
|
|
17165
|
+
}
|
|
17166
|
+
}
|
|
17167
|
+
canvas.width = width;
|
|
17168
|
+
canvas.height = height;
|
|
17169
|
+
const ctx = canvas.getContext("2d");
|
|
17170
|
+
if (!ctx) {
|
|
17171
|
+
reject(new Error("Canvas context not available"));
|
|
17172
|
+
return;
|
|
17173
|
+
}
|
|
17174
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
17175
|
+
const dataUrl = canvas.toDataURL("image/jpeg", quality);
|
|
17176
|
+
resolve(dataUrl);
|
|
17177
|
+
} catch (err) {
|
|
17178
|
+
reject(err);
|
|
17179
|
+
}
|
|
17180
|
+
};
|
|
17181
|
+
img.onerror = () => reject(new Error("Failed to load image"));
|
|
17182
|
+
img.src = reader.result;
|
|
17183
|
+
};
|
|
17184
|
+
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
17185
|
+
reader.readAsDataURL(file);
|
|
17186
|
+
});
|
|
17187
|
+
}
|
|
17188
|
+
function isValidImageFile(file) {
|
|
17189
|
+
const accepted = ["image/jpeg", "image/png", "image/webp", "image/avif"];
|
|
17190
|
+
return accepted.includes(file.type);
|
|
17191
|
+
}
|
|
17192
|
+
async function checkAgeBeforeUpload(imageOrBase64, apiUrl, apiKey) {
|
|
17193
|
+
try {
|
|
17194
|
+
const base64 = typeof imageOrBase64 === "string" ? imageOrBase64 : await compressImage(imageOrBase64, { maxDimension: 384, quality: 0.6 });
|
|
17195
|
+
const resp = await fetch(`${apiUrl}/api/v1/sizing/age-check`, {
|
|
17196
|
+
method: "POST",
|
|
17197
|
+
headers: {
|
|
17198
|
+
"Content-Type": "application/json",
|
|
17199
|
+
Authorization: `Bearer ${apiKey}`
|
|
17200
|
+
},
|
|
17201
|
+
body: JSON.stringify({ bodyImage: base64 })
|
|
17202
|
+
});
|
|
17203
|
+
if (!resp.ok) {
|
|
17204
|
+
console.warn(`[PS-SDK:AgeCheck] HTTP ${resp.status} — failing open`);
|
|
17205
|
+
return { isAdult: true, confidence: "low" };
|
|
17206
|
+
}
|
|
17207
|
+
const data = await resp.json();
|
|
17208
|
+
return {
|
|
17209
|
+
isAdult: data?.isAdult !== false,
|
|
17210
|
+
// anything other than explicit false → allow
|
|
17211
|
+
confidence: data?.confidence === "high" ? "high" : "low",
|
|
17212
|
+
reasoning: typeof data?.reasoning === "string" ? data.reasoning : void 0
|
|
17213
|
+
};
|
|
17214
|
+
} catch (err) {
|
|
17215
|
+
console.warn(`[PS-SDK:AgeCheck] error — failing open:`, err);
|
|
17216
|
+
return { isAdult: true, confidence: "low" };
|
|
17956
17217
|
}
|
|
17957
|
-
|
|
17958
|
-
|
|
17218
|
+
}
|
|
17219
|
+
const DB_NAME = "primestyle-photos";
|
|
17220
|
+
const STORE = "photos";
|
|
17221
|
+
const VERSION = 1;
|
|
17222
|
+
let _dbPromise = null;
|
|
17223
|
+
function openDb() {
|
|
17224
|
+
if (_dbPromise) return _dbPromise;
|
|
17225
|
+
_dbPromise = new Promise((resolve) => {
|
|
17226
|
+
try {
|
|
17227
|
+
if (typeof indexedDB === "undefined") return resolve(null);
|
|
17228
|
+
const req = indexedDB.open(DB_NAME, VERSION);
|
|
17229
|
+
req.onupgradeneeded = () => {
|
|
17230
|
+
const db2 = req.result;
|
|
17231
|
+
if (!db2.objectStoreNames.contains(STORE)) {
|
|
17232
|
+
db2.createObjectStore(STORE);
|
|
17233
|
+
}
|
|
17234
|
+
};
|
|
17235
|
+
req.onsuccess = () => resolve(req.result);
|
|
17236
|
+
req.onerror = () => resolve(null);
|
|
17237
|
+
req.onblocked = () => resolve(null);
|
|
17238
|
+
} catch {
|
|
17239
|
+
resolve(null);
|
|
17240
|
+
}
|
|
17241
|
+
});
|
|
17242
|
+
return _dbPromise;
|
|
17243
|
+
}
|
|
17244
|
+
async function savePhoto(id2, blob) {
|
|
17245
|
+
const db2 = await openDb();
|
|
17246
|
+
if (!db2) return false;
|
|
17247
|
+
return new Promise((resolve) => {
|
|
17248
|
+
try {
|
|
17249
|
+
const tx = db2.transaction(STORE, "readwrite");
|
|
17250
|
+
tx.objectStore(STORE).put(blob, id2);
|
|
17251
|
+
tx.oncomplete = () => resolve(true);
|
|
17252
|
+
tx.onerror = () => resolve(false);
|
|
17253
|
+
tx.onabort = () => resolve(false);
|
|
17254
|
+
} catch {
|
|
17255
|
+
resolve(false);
|
|
17256
|
+
}
|
|
17257
|
+
});
|
|
17258
|
+
}
|
|
17259
|
+
async function getPhoto(id2) {
|
|
17260
|
+
const db2 = await openDb();
|
|
17261
|
+
if (!db2) return null;
|
|
17262
|
+
return new Promise((resolve) => {
|
|
17263
|
+
try {
|
|
17264
|
+
const tx = db2.transaction(STORE, "readonly");
|
|
17265
|
+
const req = tx.objectStore(STORE).get(id2);
|
|
17266
|
+
req.onsuccess = () => resolve(req.result instanceof Blob ? req.result : null);
|
|
17267
|
+
req.onerror = () => resolve(null);
|
|
17268
|
+
} catch {
|
|
17269
|
+
resolve(null);
|
|
17270
|
+
}
|
|
17271
|
+
});
|
|
17272
|
+
}
|
|
17273
|
+
async function pruneToIds(keepIds) {
|
|
17274
|
+
const db2 = await openDb();
|
|
17275
|
+
if (!db2) return;
|
|
17276
|
+
return new Promise((resolve) => {
|
|
17277
|
+
try {
|
|
17278
|
+
const tx = db2.transaction(STORE, "readwrite");
|
|
17279
|
+
const store = tx.objectStore(STORE);
|
|
17280
|
+
const cursorReq = store.openKeyCursor();
|
|
17281
|
+
cursorReq.onsuccess = () => {
|
|
17282
|
+
const cursor = cursorReq.result;
|
|
17283
|
+
if (!cursor) return;
|
|
17284
|
+
const key = String(cursor.key);
|
|
17285
|
+
if (!keepIds.has(key)) cursor.delete();
|
|
17286
|
+
cursor.continue();
|
|
17287
|
+
};
|
|
17288
|
+
tx.oncomplete = () => resolve();
|
|
17289
|
+
tx.onerror = () => resolve();
|
|
17290
|
+
} catch {
|
|
17291
|
+
resolve();
|
|
17292
|
+
}
|
|
17293
|
+
});
|
|
17294
|
+
}
|
|
17295
|
+
const LEFT_SHOULDER = 11;
|
|
17296
|
+
const RIGHT_SHOULDER = 12;
|
|
17297
|
+
const LEFT_ELBOW = 13;
|
|
17298
|
+
const RIGHT_ELBOW = 14;
|
|
17299
|
+
const LEFT_WRIST = 15;
|
|
17300
|
+
const RIGHT_WRIST = 16;
|
|
17301
|
+
const LEFT_HIP = 23;
|
|
17302
|
+
const RIGHT_HIP = 24;
|
|
17303
|
+
const LEFT_KNEE = 25;
|
|
17304
|
+
const RIGHT_KNEE = 26;
|
|
17305
|
+
const LEFT_ANKLE = 27;
|
|
17306
|
+
const RIGHT_ANKLE = 28;
|
|
17307
|
+
const NOSE = 0;
|
|
17308
|
+
let poseLandmarker = null;
|
|
17309
|
+
let loadingPromise$1 = null;
|
|
17310
|
+
async function loadMediaPipe() {
|
|
17311
|
+
if (poseLandmarker) return;
|
|
17312
|
+
if (loadingPromise$1) return loadingPromise$1;
|
|
17313
|
+
loadingPromise$1 = (async () => {
|
|
17314
|
+
const vision = await import(
|
|
17315
|
+
/* webpackIgnore: true */
|
|
17316
|
+
// @ts-ignore dynamic CDN import
|
|
17317
|
+
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/vision_bundle.mjs"
|
|
17318
|
+
);
|
|
17319
|
+
const { FilesetResolver, PoseLandmarker } = vision;
|
|
17320
|
+
const filesetResolver = await FilesetResolver.forVisionTasks(
|
|
17321
|
+
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/wasm"
|
|
17322
|
+
);
|
|
17323
|
+
poseLandmarker = await PoseLandmarker.createFromOptions(filesetResolver, {
|
|
17324
|
+
baseOptions: {
|
|
17325
|
+
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task",
|
|
17326
|
+
delegate: "GPU"
|
|
17327
|
+
},
|
|
17328
|
+
runningMode: "IMAGE",
|
|
17329
|
+
numPoses: 1
|
|
17330
|
+
});
|
|
17331
|
+
})();
|
|
17332
|
+
return loadingPromise$1;
|
|
17333
|
+
}
|
|
17334
|
+
async function detectMeasurementLines(imageSrc) {
|
|
17335
|
+
try {
|
|
17336
|
+
await loadMediaPipe();
|
|
17337
|
+
const img = await loadImage$1(imageSrc);
|
|
17338
|
+
const result = poseLandmarker.detect(img);
|
|
17339
|
+
if (!result?.landmarks?.length || result.landmarks[0].length < 25) {
|
|
17340
|
+
return null;
|
|
17341
|
+
}
|
|
17342
|
+
const landmarks = result.landmarks[0];
|
|
17343
|
+
const lShoulder = landmarks[LEFT_SHOULDER];
|
|
17344
|
+
const rShoulder = landmarks[RIGHT_SHOULDER];
|
|
17345
|
+
const lHip = landmarks[LEFT_HIP];
|
|
17346
|
+
const rHip = landmarks[RIGHT_HIP];
|
|
17347
|
+
const shoulderY = (lShoulder.y + rShoulder.y) / 2;
|
|
17348
|
+
const hipY = (lHip.y + rHip.y) / 2;
|
|
17349
|
+
const torsoLen = hipY - shoulderY;
|
|
17350
|
+
const chestY = shoulderY + torsoLen * 0.28;
|
|
17351
|
+
const shoulderWidth = Math.abs(rShoulder.x - lShoulder.x);
|
|
17352
|
+
const chestX1 = Math.min(lShoulder.x, rShoulder.x) - shoulderWidth * 0.05;
|
|
17353
|
+
const chestX2 = Math.max(lShoulder.x, rShoulder.x) + shoulderWidth * 0.05;
|
|
17354
|
+
const hipWidth = Math.abs(rHip.x - lHip.x);
|
|
17355
|
+
const hipX1 = Math.min(lHip.x, rHip.x) - hipWidth * 0.15;
|
|
17356
|
+
const hipX2 = Math.max(lHip.x, rHip.x) + hipWidth * 0.15;
|
|
17357
|
+
const waistY = chestY + (hipY - chestY) * 0.55;
|
|
17358
|
+
const waistWidth = shoulderWidth * 0.85;
|
|
17359
|
+
const waistCenterX = (lShoulder.x + rShoulder.x + lHip.x + rHip.x) / 4;
|
|
17360
|
+
const waistX1 = waistCenterX - waistWidth / 2;
|
|
17361
|
+
const waistX2 = waistCenterX + waistWidth / 2;
|
|
17362
|
+
return {
|
|
17363
|
+
chest: { y: chestY, x1: chestX1, x2: chestX2 },
|
|
17364
|
+
waist: { y: waistY, x1: waistX1, x2: waistX2 },
|
|
17365
|
+
hips: { y: hipY, x1: hipX1, x2: hipX2 }
|
|
17366
|
+
};
|
|
17367
|
+
} catch (err) {
|
|
17368
|
+
console.error("[PS-SDK] Pose detection failed:", err);
|
|
17369
|
+
return null;
|
|
17959
17370
|
}
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
|
|
17371
|
+
}
|
|
17372
|
+
function loadImage$1(src) {
|
|
17373
|
+
return new Promise((resolve, reject) => {
|
|
17374
|
+
const img = new Image();
|
|
17375
|
+
img.crossOrigin = "anonymous";
|
|
17376
|
+
img.onload = () => resolve(img);
|
|
17377
|
+
img.onerror = () => {
|
|
17378
|
+
const img2 = new Image();
|
|
17379
|
+
img2.onload = () => resolve(img2);
|
|
17380
|
+
img2.onerror = () => reject(new Error("Failed to load image"));
|
|
17381
|
+
img2.src = src;
|
|
17382
|
+
};
|
|
17383
|
+
img.src = src;
|
|
17384
|
+
});
|
|
17385
|
+
}
|
|
17386
|
+
async function downsampleIfNeeded(img, maxLongEdge = 1024) {
|
|
17387
|
+
const w2 = img.naturalWidth || img.width;
|
|
17388
|
+
const h = img.naturalHeight || img.height;
|
|
17389
|
+
const longEdge = Math.max(w2, h);
|
|
17390
|
+
if (longEdge <= maxLongEdge) return img;
|
|
17391
|
+
const scale = maxLongEdge / longEdge;
|
|
17392
|
+
const newW = Math.round(w2 * scale);
|
|
17393
|
+
const newH = Math.round(h * scale);
|
|
17394
|
+
const canvas = document.createElement("canvas");
|
|
17395
|
+
canvas.width = newW;
|
|
17396
|
+
canvas.height = newH;
|
|
17397
|
+
const ctx = canvas.getContext("2d");
|
|
17398
|
+
if (!ctx) return img;
|
|
17399
|
+
ctx.imageSmoothingQuality = "high";
|
|
17400
|
+
ctx.drawImage(img, 0, 0, newW, newH);
|
|
17401
|
+
console.log(`[PS-SDK:Downsample] ${w2}×${h} → ${newW}×${newH} (max long edge ${maxLongEdge}px)`);
|
|
17402
|
+
return canvas;
|
|
17403
|
+
}
|
|
17404
|
+
async function detectBodyLandmarks(imageSrc) {
|
|
17405
|
+
try {
|
|
17406
|
+
await loadMediaPipe();
|
|
17407
|
+
let img;
|
|
17408
|
+
if (typeof imageSrc === "string") {
|
|
17409
|
+
img = await loadImage$1(imageSrc);
|
|
17410
|
+
} else {
|
|
17411
|
+
img = imageSrc;
|
|
17412
|
+
}
|
|
17413
|
+
const inputForDetect = await downsampleIfNeeded(img, 1024);
|
|
17414
|
+
const workingW = inputForDetect.width || img.naturalWidth || img.width;
|
|
17415
|
+
const workingH = inputForDetect.height || img.naturalHeight || img.height;
|
|
17416
|
+
const result = poseLandmarker.detect(inputForDetect);
|
|
17417
|
+
if (!result?.landmarks?.length || result.landmarks[0].length < 29) {
|
|
17418
|
+
console.warn("[PS-SDK:MediaPipe] detection returned no usable landmarks", {
|
|
17419
|
+
gotLandmarks: !!result?.landmarks?.length,
|
|
17420
|
+
jointCount: result?.landmarks?.[0]?.length ?? 0
|
|
17421
|
+
});
|
|
17422
|
+
return null;
|
|
17423
|
+
}
|
|
17424
|
+
const lm = result.landmarks[0];
|
|
17425
|
+
const wlm = result.worldLandmarks?.[0] ?? null;
|
|
17426
|
+
const pickPoint = (idx, useWorld = false) => {
|
|
17427
|
+
const src = useWorld && wlm ? wlm[idx] : lm[idx];
|
|
17428
|
+
const out2 = { x: src.x, y: src.y };
|
|
17429
|
+
if (typeof src.z === "number") out2.z = src.z;
|
|
17430
|
+
if (typeof src.visibility === "number") out2.visibility = src.visibility;
|
|
17431
|
+
return out2;
|
|
17432
|
+
};
|
|
17433
|
+
const out = {
|
|
17434
|
+
leftShoulder: pickPoint(LEFT_SHOULDER),
|
|
17435
|
+
rightShoulder: pickPoint(RIGHT_SHOULDER),
|
|
17436
|
+
leftHip: pickPoint(LEFT_HIP),
|
|
17437
|
+
rightHip: pickPoint(RIGHT_HIP),
|
|
17438
|
+
leftElbow: pickPoint(LEFT_ELBOW),
|
|
17439
|
+
rightElbow: pickPoint(RIGHT_ELBOW),
|
|
17440
|
+
leftWrist: pickPoint(LEFT_WRIST),
|
|
17441
|
+
rightWrist: pickPoint(RIGHT_WRIST),
|
|
17442
|
+
leftKnee: pickPoint(LEFT_KNEE),
|
|
17443
|
+
rightKnee: pickPoint(RIGHT_KNEE),
|
|
17444
|
+
leftAnkle: pickPoint(LEFT_ANKLE),
|
|
17445
|
+
rightAnkle: pickPoint(RIGHT_ANKLE),
|
|
17446
|
+
nose: pickPoint(NOSE),
|
|
17447
|
+
imageWidth: workingW,
|
|
17448
|
+
imageHeight: workingH
|
|
17449
|
+
};
|
|
17450
|
+
const isJointPoint = (v2) => !!v2 && typeof v2 === "object" && typeof v2.x === "number" && typeof v2.y === "number";
|
|
17451
|
+
const jointCount = Object.values(out).filter(isJointPoint).length;
|
|
17452
|
+
const hasZ = lm.some((p2) => typeof p2.z === "number");
|
|
17453
|
+
const hasWorld = !!wlm;
|
|
17454
|
+
console.groupCollapsed(`[PS-SDK:MediaPipe] ✓ ${jointCount} joints detected @ ${out.imageWidth}×${out.imageHeight}px (z=${hasZ ? "yes" : "no"}, worldLandmarks=${hasWorld ? "yes" : "no"})`);
|
|
17455
|
+
for (const [name, v2] of Object.entries(out)) {
|
|
17456
|
+
if (!isJointPoint(v2)) continue;
|
|
17457
|
+
const p2 = v2;
|
|
17458
|
+
const z2 = typeof p2.z === "number" ? `, z=${p2.z.toFixed(3)}` : "";
|
|
17459
|
+
const vis = typeof p2.visibility === "number" ? `, vis=${p2.visibility.toFixed(2)}` : "";
|
|
17460
|
+
console.log(` ${name.padEnd(15)} (${p2.x.toFixed(3)}, ${p2.y.toFixed(3)}${z2}${vis})`);
|
|
17461
|
+
}
|
|
17462
|
+
console.log(` imageWidth ${out.imageWidth}`);
|
|
17463
|
+
console.log(` imageHeight ${out.imageHeight}`);
|
|
17464
|
+
console.groupEnd();
|
|
17465
|
+
return out;
|
|
17466
|
+
} catch (err) {
|
|
17467
|
+
console.error("[PS-SDK:MediaPipe] Body landmark detection failed:", err);
|
|
17468
|
+
return null;
|
|
17965
17469
|
}
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
17970
|
-
|
|
17971
|
-
|
|
17972
|
-
|
|
17470
|
+
}
|
|
17471
|
+
const IDX = {
|
|
17472
|
+
noseTip: 1,
|
|
17473
|
+
noseBridge: 168,
|
|
17474
|
+
leftInnerEye: 133,
|
|
17475
|
+
rightInnerEye: 362,
|
|
17476
|
+
leftOuterEye: 33,
|
|
17477
|
+
rightOuterEye: 263,
|
|
17478
|
+
// Iris landmarks (478-point model with iris refinement)
|
|
17479
|
+
leftIrisCenter: 468,
|
|
17480
|
+
leftIrisRing: [469, 470, 471, 472],
|
|
17481
|
+
rightIrisCenter: 473,
|
|
17482
|
+
rightIrisRing: [474, 475, 476, 477],
|
|
17483
|
+
// Tragus (ear attach point) — best approximations in the face mesh
|
|
17484
|
+
leftTragus: 234,
|
|
17485
|
+
rightTragus: 454,
|
|
17486
|
+
forehead: 10,
|
|
17487
|
+
chin: 152,
|
|
17488
|
+
leftMouth: 61,
|
|
17489
|
+
rightMouth: 291
|
|
17490
|
+
};
|
|
17491
|
+
const IRIS_DIAMETER_MM = 11.7;
|
|
17492
|
+
let faceLandmarker = null;
|
|
17493
|
+
let loadingPromise = null;
|
|
17494
|
+
async function loadFaceLandmarker() {
|
|
17495
|
+
if (faceLandmarker) {
|
|
17496
|
+
console.log("[PS-SDK:FaceDetect] loadFaceLandmarker — already loaded, reusing");
|
|
17497
|
+
return;
|
|
17973
17498
|
}
|
|
17974
|
-
|
|
17975
|
-
|
|
17499
|
+
if (loadingPromise) {
|
|
17500
|
+
console.log("[PS-SDK:FaceDetect] loadFaceLandmarker — awaiting in-flight load");
|
|
17501
|
+
return loadingPromise;
|
|
17976
17502
|
}
|
|
17977
|
-
.
|
|
17978
|
-
|
|
17979
|
-
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17503
|
+
console.log("[PS-SDK:FaceDetect] loadFaceLandmarker — starting fresh init");
|
|
17504
|
+
loadingPromise = (async () => {
|
|
17505
|
+
try {
|
|
17506
|
+
console.log("[PS-SDK:FaceDetect] importing vision_bundle.mjs from CDN…");
|
|
17507
|
+
const t0 = performance.now();
|
|
17508
|
+
const vision = await import(
|
|
17509
|
+
/* webpackIgnore: true */
|
|
17510
|
+
// @ts-ignore dynamic CDN import
|
|
17511
|
+
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/vision_bundle.mjs"
|
|
17512
|
+
);
|
|
17513
|
+
console.log(`[PS-SDK:FaceDetect] vision bundle imported in ${Math.round(performance.now() - t0)}ms`);
|
|
17514
|
+
const { FilesetResolver, FaceLandmarker } = vision;
|
|
17515
|
+
if (!FilesetResolver || !FaceLandmarker) {
|
|
17516
|
+
console.error("[PS-SDK:FaceDetect] vision bundle missing expected exports", { keys: Object.keys(vision) });
|
|
17517
|
+
throw new Error("MediaPipe vision bundle returned no FilesetResolver/FaceLandmarker");
|
|
17518
|
+
}
|
|
17519
|
+
console.log("[PS-SDK:FaceDetect] resolving WASM filesets…");
|
|
17520
|
+
const t1 = performance.now();
|
|
17521
|
+
const filesetResolver = await FilesetResolver.forVisionTasks(
|
|
17522
|
+
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.33/wasm"
|
|
17523
|
+
);
|
|
17524
|
+
console.log(`[PS-SDK:FaceDetect] WASM resolver ready in ${Math.round(performance.now() - t1)}ms`);
|
|
17525
|
+
console.log("[PS-SDK:FaceDetect] creating FaceLandmarker (GPU delegate, model=latest)…");
|
|
17526
|
+
const t2 = performance.now();
|
|
17527
|
+
faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver, {
|
|
17528
|
+
baseOptions: {
|
|
17529
|
+
modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
|
|
17530
|
+
delegate: "GPU"
|
|
17531
|
+
},
|
|
17532
|
+
runningMode: "IMAGE",
|
|
17533
|
+
numFaces: 1,
|
|
17534
|
+
outputFaceBlendshapes: false,
|
|
17535
|
+
outputFacialTransformationMatrixes: false
|
|
17536
|
+
});
|
|
17537
|
+
console.log(`[PS-SDK:FaceDetect] FaceLandmarker ready in ${Math.round(performance.now() - t2)}ms`);
|
|
17538
|
+
} catch (err) {
|
|
17539
|
+
console.error("[PS-SDK:FaceDetect] loadFaceLandmarker FAILED:", err);
|
|
17540
|
+
loadingPromise = null;
|
|
17541
|
+
throw err;
|
|
17542
|
+
}
|
|
17543
|
+
})();
|
|
17544
|
+
return loadingPromise;
|
|
17545
|
+
}
|
|
17546
|
+
function loadImage(src) {
|
|
17547
|
+
return new Promise((resolve, reject) => {
|
|
17548
|
+
const img = new Image();
|
|
17549
|
+
img.crossOrigin = "anonymous";
|
|
17550
|
+
img.onload = () => {
|
|
17551
|
+
console.log("[PS-SDK:FaceDetect] loadImage — primary (crossOrigin) succeeded", { w: img.naturalWidth, h: img.naturalHeight });
|
|
17552
|
+
resolve(img);
|
|
17553
|
+
};
|
|
17554
|
+
img.onerror = () => {
|
|
17555
|
+
console.warn("[PS-SDK:FaceDetect] loadImage — primary (crossOrigin) failed, retrying without crossOrigin");
|
|
17556
|
+
const img2 = new Image();
|
|
17557
|
+
img2.onload = () => {
|
|
17558
|
+
console.log("[PS-SDK:FaceDetect] loadImage — fallback succeeded", { w: img2.naturalWidth, h: img2.naturalHeight });
|
|
17559
|
+
resolve(img2);
|
|
17560
|
+
};
|
|
17561
|
+
img2.onerror = () => {
|
|
17562
|
+
console.error("[PS-SDK:FaceDetect] loadImage — both attempts failed for src:", src.slice(0, 120));
|
|
17563
|
+
reject(new Error("Failed to load image"));
|
|
17564
|
+
};
|
|
17565
|
+
img2.src = src;
|
|
17566
|
+
};
|
|
17567
|
+
img.src = src;
|
|
17568
|
+
});
|
|
17569
|
+
}
|
|
17570
|
+
function irisDiameterPx(ring, imageWidth, imageHeight) {
|
|
17571
|
+
if (ring.length < 4) return 0;
|
|
17572
|
+
const toPx = (p4) => ({ x: p4.x * imageWidth, y: p4.y * imageHeight });
|
|
17573
|
+
const [p0, p1, p2, p3] = ring.map(toPx);
|
|
17574
|
+
const d1 = Math.hypot(p0.x - p2.x, p0.y - p2.y);
|
|
17575
|
+
const d2 = Math.hypot(p1.x - p3.x, p1.y - p3.y);
|
|
17576
|
+
return (d1 + d2) / 2;
|
|
17577
|
+
}
|
|
17578
|
+
function extractLandmarks(raw) {
|
|
17579
|
+
if (raw.length < 478) {
|
|
17580
|
+
console.warn(`[PS-SDK:FaceDetect] extractLandmarks REJECTED — need 478 landmarks (iris refinement), got ${raw.length}. The model is missing iris data — probably the CDN "latest" tag moved to a non-iris variant.`);
|
|
17581
|
+
return null;
|
|
17983
17582
|
}
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
17987
|
-
|
|
17988
|
-
|
|
17583
|
+
const at = (i) => ({ x: raw[i].x, y: raw[i].y, z: raw[i].z });
|
|
17584
|
+
return {
|
|
17585
|
+
noseTip: at(IDX.noseTip),
|
|
17586
|
+
noseBridge: at(IDX.noseBridge),
|
|
17587
|
+
leftInnerEye: at(IDX.leftInnerEye),
|
|
17588
|
+
rightInnerEye: at(IDX.rightInnerEye),
|
|
17589
|
+
leftOuterEye: at(IDX.leftOuterEye),
|
|
17590
|
+
rightOuterEye: at(IDX.rightOuterEye),
|
|
17591
|
+
leftIrisCenter: at(IDX.leftIrisCenter),
|
|
17592
|
+
rightIrisCenter: at(IDX.rightIrisCenter),
|
|
17593
|
+
leftIrisRing: IDX.leftIrisRing.map(at),
|
|
17594
|
+
rightIrisRing: IDX.rightIrisRing.map(at),
|
|
17595
|
+
leftTragus: at(IDX.leftTragus),
|
|
17596
|
+
rightTragus: at(IDX.rightTragus),
|
|
17597
|
+
forehead: at(IDX.forehead),
|
|
17598
|
+
chin: at(IDX.chin),
|
|
17599
|
+
leftMouth: at(IDX.leftMouth),
|
|
17600
|
+
rightMouth: at(IDX.rightMouth)
|
|
17601
|
+
};
|
|
17602
|
+
}
|
|
17603
|
+
function computeMeasurements(lm, imageWidth, imageHeight) {
|
|
17604
|
+
const leftPx = irisDiameterPx(lm.leftIrisRing, imageWidth, imageHeight);
|
|
17605
|
+
const rightPx = irisDiameterPx(lm.rightIrisRing, imageWidth, imageHeight);
|
|
17606
|
+
const irisPx = (leftPx + rightPx) / 2;
|
|
17607
|
+
let irisConfidence = 1;
|
|
17608
|
+
if (irisPx < 8) irisConfidence = 0.3;
|
|
17609
|
+
else if (Math.abs(leftPx - rightPx) / irisPx > 0.3) irisConfidence = 0.5;
|
|
17610
|
+
else if (Math.abs(leftPx - rightPx) / irisPx > 0.15) irisConfidence = 0.8;
|
|
17611
|
+
const pxToMm = irisPx > 0 ? IRIS_DIAMETER_MM / irisPx : 0;
|
|
17612
|
+
const mmBetween = (a2, b2) => {
|
|
17613
|
+
const pxDist = Math.hypot(
|
|
17614
|
+
(a2.x - b2.x) * imageWidth,
|
|
17615
|
+
(a2.y - b2.y) * imageHeight
|
|
17616
|
+
);
|
|
17617
|
+
return pxDist * pxToMm;
|
|
17618
|
+
};
|
|
17619
|
+
const pd2 = mmBetween(lm.leftIrisCenter, lm.rightIrisCenter);
|
|
17620
|
+
const innerEyeGap = mmBetween(lm.leftInnerEye, lm.rightInnerEye);
|
|
17621
|
+
const bridgeWidth = innerEyeGap / 1.6;
|
|
17622
|
+
const faceWidth = mmBetween(lm.leftTragus, lm.rightTragus);
|
|
17623
|
+
const templeLengthLeft = mmBetween(lm.leftTragus, lm.leftOuterEye);
|
|
17624
|
+
const templeLengthRight = mmBetween(lm.rightTragus, lm.rightOuterEye);
|
|
17625
|
+
const templeLength = (templeLengthLeft + templeLengthRight) / 2;
|
|
17626
|
+
const headWidth = faceWidth * 1.07;
|
|
17627
|
+
const zDepthNorm = Math.abs((lm.forehead.z ?? 0) - (lm.chin.z ?? 0));
|
|
17628
|
+
const rawHeadDepthMm = zDepthNorm * imageWidth * pxToMm;
|
|
17629
|
+
const headDepth = Math.max(170, Math.min(210, rawHeadDepthMm || 190));
|
|
17630
|
+
const a = headWidth / 2;
|
|
17631
|
+
const b = headDepth / 2;
|
|
17632
|
+
const headCircumference = Math.PI * Math.sqrt(2 * (a * a + b * b));
|
|
17633
|
+
return {
|
|
17634
|
+
measurements: {
|
|
17635
|
+
irisDiameter: IRIS_DIAMETER_MM,
|
|
17636
|
+
pd: round1(pd2),
|
|
17637
|
+
bridgeWidth: round1(bridgeWidth),
|
|
17638
|
+
faceWidth: round1(faceWidth),
|
|
17639
|
+
templeLengthLeft: round1(templeLengthLeft),
|
|
17640
|
+
templeLengthRight: round1(templeLengthRight),
|
|
17641
|
+
templeLength: round1(templeLength),
|
|
17642
|
+
headWidth: round1(headWidth),
|
|
17643
|
+
headDepth: round1(headDepth),
|
|
17644
|
+
headCircumference: round1(headCircumference)
|
|
17645
|
+
},
|
|
17646
|
+
irisConfidence
|
|
17647
|
+
};
|
|
17648
|
+
}
|
|
17649
|
+
function round1(n2) {
|
|
17650
|
+
return Math.round(n2 * 10) / 10;
|
|
17651
|
+
}
|
|
17652
|
+
async function detectFaceMeasurements(imageSrc) {
|
|
17653
|
+
console.log("[PS-SDK:FaceDetect] detectFaceMeasurements called", {
|
|
17654
|
+
srcType: typeof imageSrc === "string" ? imageSrc.startsWith("blob:") ? "blob-url" : imageSrc.startsWith("data:") ? "data-url" : "other" : "HTMLImageElement",
|
|
17655
|
+
srcLength: typeof imageSrc === "string" ? imageSrc.length : "N/A"
|
|
17656
|
+
});
|
|
17657
|
+
const tStart = performance.now();
|
|
17658
|
+
try {
|
|
17659
|
+
console.log("[PS-SDK:FaceDetect] step 1/4 — loading FaceLandmarker");
|
|
17660
|
+
await loadFaceLandmarker();
|
|
17661
|
+
console.log("[PS-SDK:FaceDetect] step 2/4 — loading image");
|
|
17662
|
+
const img = typeof imageSrc === "string" ? await loadImage(imageSrc) : imageSrc;
|
|
17663
|
+
const imageWidth = img.naturalWidth || img.width;
|
|
17664
|
+
const imageHeight = img.naturalHeight || img.height;
|
|
17665
|
+
console.log("[PS-SDK:FaceDetect] step 3/4 — running faceLandmarker.detect()", { imageWidth, imageHeight });
|
|
17666
|
+
const tDetect = performance.now();
|
|
17667
|
+
const result = faceLandmarker.detect(img);
|
|
17668
|
+
const detectMs = Math.round(performance.now() - tDetect);
|
|
17669
|
+
const faceCount = result?.faceLandmarks?.length ?? 0;
|
|
17670
|
+
console.log(`[PS-SDK:FaceDetect] detect() returned in ${detectMs}ms — faces: ${faceCount}`);
|
|
17671
|
+
if (!faceCount) {
|
|
17672
|
+
console.warn("[PS-SDK:FaceDetect] NO FACES detected — returning null");
|
|
17673
|
+
return null;
|
|
17674
|
+
}
|
|
17675
|
+
const raw = result.faceLandmarks[0];
|
|
17676
|
+
console.log(`[PS-SDK:FaceDetect] first face has ${raw.length} landmarks`);
|
|
17677
|
+
const landmarks = extractLandmarks(raw);
|
|
17678
|
+
if (!landmarks) {
|
|
17679
|
+
return null;
|
|
17680
|
+
}
|
|
17681
|
+
console.log("[PS-SDK:FaceDetect] step 4/4 — computing measurements");
|
|
17682
|
+
const { measurements, irisConfidence } = computeMeasurements(landmarks, imageWidth, imageHeight);
|
|
17683
|
+
console.log(`[PS-SDK:FaceDetect] ✓ DONE in ${Math.round(performance.now() - tStart)}ms`, { irisConfidence, measurements });
|
|
17684
|
+
return { landmarks, measurementsMm: measurements, irisConfidence, imageWidth, imageHeight };
|
|
17685
|
+
} catch (err) {
|
|
17686
|
+
console.error("[PS-SDK:FaceDetect] detectFaceMeasurements FAILED:", err);
|
|
17687
|
+
return null;
|
|
17989
17688
|
}
|
|
17990
|
-
|
|
17991
|
-
|
|
17992
|
-
.
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
17998
|
-
|
|
17689
|
+
}
|
|
17690
|
+
function parseRange(s) {
|
|
17691
|
+
const ns = s.replace(/[^\d.\-–]/g, " ").trim().split(/[\s\-–]+/).filter(Boolean).map(Number).filter((n2) => !isNaN(n2));
|
|
17692
|
+
return ns.length ? { min: Math.min(...ns), max: Math.max(...ns) } : { min: 0, max: 0 };
|
|
17693
|
+
}
|
|
17694
|
+
function parseNum(s) {
|
|
17695
|
+
const n2 = parseFloat(s.replace(/[^\d.]/g, ""));
|
|
17696
|
+
return isNaN(n2) ? 0 : n2;
|
|
17697
|
+
}
|
|
17698
|
+
function computeFit(userValue, chartRange, unit) {
|
|
17699
|
+
const { min: rMin, max: rMax } = parseRange(chartRange);
|
|
17700
|
+
if (rMin === 0 && rMax === 0) return "good";
|
|
17701
|
+
const range = rMax - rMin;
|
|
17702
|
+
const threshold = range > 0 ? range * 0.5 : rMin * 0.05 || 3;
|
|
17703
|
+
const perfectTol = unit === "cm" ? 2.54 : unit === "mm" ? 25.4 : 1;
|
|
17704
|
+
if (userValue > rMin - perfectTol && userValue < rMax + perfectTol) return "good";
|
|
17705
|
+
if (userValue < rMin) {
|
|
17706
|
+
const diff2 = rMin - userValue;
|
|
17707
|
+
if (diff2 > threshold * 2) return "too-loose";
|
|
17708
|
+
if (diff2 > threshold) return "loose";
|
|
17709
|
+
return "a-bit-loose";
|
|
17999
17710
|
}
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
17711
|
+
const diff = userValue - rMax;
|
|
17712
|
+
if (diff > threshold * 2) return "too-tight";
|
|
17713
|
+
if (diff > threshold) return "tight";
|
|
17714
|
+
return "a-bit-tight";
|
|
17715
|
+
}
|
|
17716
|
+
const AREA_TO_POSE_KEY = {
|
|
17717
|
+
chest: "chest",
|
|
17718
|
+
bust: "chest",
|
|
17719
|
+
waist: "waist",
|
|
17720
|
+
hips: "hips",
|
|
17721
|
+
hip: "hips"
|
|
17722
|
+
};
|
|
17723
|
+
const SKIP_AREAS_FOR_FIT = /* @__PURE__ */ new Set([
|
|
17724
|
+
"height",
|
|
17725
|
+
"altezza",
|
|
17726
|
+
"estatura"
|
|
17727
|
+
]);
|
|
17728
|
+
function buildFitInfo(matchDetails, poseLines, unit) {
|
|
17729
|
+
return matchDetails.filter((m2) => !SKIP_AREAS_FOR_FIT.has(m2.measurement.toLowerCase().replace(/\s*\(.*?\)\s*/g, "").trim())).map((m2) => {
|
|
17730
|
+
const userNum = parseNum(m2.userValue);
|
|
17731
|
+
const fit = computeFit(userNum, m2.chartRange, unit);
|
|
17732
|
+
const info = {
|
|
17733
|
+
area: m2.measurement,
|
|
17734
|
+
section: m2.section || void 0,
|
|
17735
|
+
fit,
|
|
17736
|
+
userValue: userNum || void 0,
|
|
17737
|
+
garmentRange: m2.chartRange || void 0
|
|
17738
|
+
};
|
|
17739
|
+
if (poseLines) {
|
|
17740
|
+
const poseKey = AREA_TO_POSE_KEY[m2.measurement.toLowerCase()];
|
|
17741
|
+
if (poseKey && poseLines[poseKey]) {
|
|
17742
|
+
const line = poseLines[poseKey];
|
|
17743
|
+
info.y = Math.round(line.y * 1e3) / 1e3;
|
|
17744
|
+
info.x1 = Math.round(line.x1 * 1e3) / 1e3;
|
|
17745
|
+
info.x2 = Math.round(line.x2 * 1e3) / 1e3;
|
|
17746
|
+
}
|
|
17747
|
+
}
|
|
17748
|
+
return info;
|
|
17749
|
+
});
|
|
17750
|
+
}
|
|
17751
|
+
function buildSilhouetteContext(sizingResult, sizeGuide, selectedSizeOverride) {
|
|
17752
|
+
if (!sizingResult && !sizeGuide) return void 0;
|
|
17753
|
+
const out = {};
|
|
17754
|
+
if (selectedSizeOverride) out.recommendedSize = selectedSizeOverride;
|
|
17755
|
+
else if (sizingResult?.recommendedSize) out.recommendedSize = sizingResult.recommendedSize;
|
|
17756
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17757
|
+
const userLines = [];
|
|
17758
|
+
const push = (md2) => {
|
|
17759
|
+
if (!md2) return;
|
|
17760
|
+
for (const m2 of md2) {
|
|
17761
|
+
const k2 = m2.measurement.toLowerCase();
|
|
17762
|
+
if (seen.has(k2)) continue;
|
|
17763
|
+
seen.add(k2);
|
|
17764
|
+
if (m2.userValue) userLines.push(`${m2.measurement} ${m2.userValue}`);
|
|
17765
|
+
}
|
|
17766
|
+
};
|
|
17767
|
+
push(sizingResult?.matchDetails);
|
|
17768
|
+
if (sizingResult?.sections) {
|
|
17769
|
+
for (const sec of Object.values(sizingResult.sections)) push(sec.matchDetails);
|
|
18003
17770
|
}
|
|
18004
|
-
.
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
border-radius: 0.5vw; background: rgba(33,84,239,0.06);
|
|
17771
|
+
if (userLines.length) out.userMeasurementsText = userLines.join(", ");
|
|
17772
|
+
if (sizeGuide?.headers?.length && sizeGuide.rows?.length) {
|
|
17773
|
+
const hdrs = sizeGuide.headers;
|
|
17774
|
+
const rowStrings = sizeGuide.rows.slice(0, 20).map(
|
|
17775
|
+
(row) => hdrs.map((h, i) => `${h} ${row[i] ?? "—"}`).join(", ")
|
|
17776
|
+
);
|
|
17777
|
+
out.sizeChartSummary = rowStrings.join(" | ");
|
|
18012
17778
|
}
|
|
18013
|
-
.
|
|
18014
|
-
|
|
18015
|
-
|
|
17779
|
+
return Object.keys(out).length ? out : void 0;
|
|
17780
|
+
}
|
|
17781
|
+
const FALLBACK_FIELDS_FEMALE = [
|
|
17782
|
+
{ key: "bust", label: "Bust", required: true, unit: "cm", placeholder: "e.g. 88", category: "body" },
|
|
17783
|
+
{ key: "waist", label: "Waist", required: true, unit: "cm", placeholder: "e.g. 70", category: "body" },
|
|
17784
|
+
{ key: "hips", label: "Hips", required: true, unit: "cm", placeholder: "e.g. 96", category: "body" },
|
|
17785
|
+
{ key: "shoulderWidth", label: "Shoulders", required: false, unit: "cm", placeholder: "e.g. 39", category: "body" },
|
|
17786
|
+
{ key: "inseam", label: "Inseam", required: false, unit: "cm", placeholder: "e.g. 76", category: "body" },
|
|
17787
|
+
{ key: "footLengthCm", label: "Foot length", required: false, unit: "cm", placeholder: "e.g. 24", category: "shoe" }
|
|
17788
|
+
];
|
|
17789
|
+
const FALLBACK_FIELDS_MALE = [
|
|
17790
|
+
{ key: "chest", label: "Chest", required: true, unit: "cm", placeholder: "e.g. 104", category: "body" },
|
|
17791
|
+
{ key: "waist", label: "Waist", required: true, unit: "cm", placeholder: "e.g. 84", category: "body" },
|
|
17792
|
+
{ key: "shoulderWidth", label: "Shoulders", required: false, unit: "cm", placeholder: "e.g. 46", category: "body" },
|
|
17793
|
+
{ key: "sleeveLength", label: "Sleeve", required: false, unit: "cm", placeholder: "e.g. 64", category: "body" },
|
|
17794
|
+
{ key: "inseam", label: "Inseam", required: false, unit: "cm", placeholder: "e.g. 81", category: "body" },
|
|
17795
|
+
{ key: "footLengthCm", label: "Foot length", required: false, unit: "cm", placeholder: "e.g. 27", category: "shoe" }
|
|
17796
|
+
];
|
|
17797
|
+
const TOTAL_STEPS = 3;
|
|
17798
|
+
const LS_PREFIX = "primestyle_";
|
|
17799
|
+
const PROFILES_KEY = "profiles";
|
|
17800
|
+
const ACTIVE_PROFILE_KEY = "active_profile_id";
|
|
17801
|
+
const PS_STORAGE_CHANGE_EVENT = "ps:storage-change";
|
|
17802
|
+
function emitStorageChange(reason) {
|
|
17803
|
+
if (typeof window === "undefined") return;
|
|
17804
|
+
try {
|
|
17805
|
+
window.dispatchEvent(new CustomEvent(PS_STORAGE_CHANGE_EVENT, { detail: { reason } }));
|
|
17806
|
+
} catch {
|
|
18016
17807
|
}
|
|
18017
|
-
|
|
18018
|
-
|
|
18019
|
-
|
|
18020
|
-
|
|
17808
|
+
}
|
|
17809
|
+
function lsGet(key, fallback) {
|
|
17810
|
+
if (typeof window === "undefined") return fallback;
|
|
17811
|
+
try {
|
|
17812
|
+
const raw = localStorage.getItem(LS_PREFIX + key);
|
|
17813
|
+
return raw ? JSON.parse(raw) : fallback;
|
|
17814
|
+
} catch {
|
|
17815
|
+
return fallback;
|
|
18021
17816
|
}
|
|
18022
|
-
|
|
18023
|
-
|
|
18024
|
-
|
|
18025
|
-
|
|
18026
|
-
|
|
18027
|
-
transition: all 0.15s; flex-shrink: 0;
|
|
17817
|
+
}
|
|
17818
|
+
function lsSet(key, value) {
|
|
17819
|
+
try {
|
|
17820
|
+
localStorage.setItem(LS_PREFIX + key, JSON.stringify(value));
|
|
17821
|
+
} catch {
|
|
18028
17822
|
}
|
|
18029
|
-
|
|
18030
|
-
|
|
18031
|
-
|
|
18032
|
-
|
|
18033
|
-
|
|
17823
|
+
}
|
|
17824
|
+
function getProfiles() {
|
|
17825
|
+
return lsGet(PROFILES_KEY, []);
|
|
17826
|
+
}
|
|
17827
|
+
function saveProfiles(profiles) {
|
|
17828
|
+
lsSet(PROFILES_KEY, profiles);
|
|
17829
|
+
emitStorageChange("profiles");
|
|
17830
|
+
}
|
|
17831
|
+
function getActiveProfileId() {
|
|
17832
|
+
return lsGet(ACTIVE_PROFILE_KEY, null);
|
|
17833
|
+
}
|
|
17834
|
+
function setActiveProfileId(id2) {
|
|
17835
|
+
lsSet(ACTIVE_PROFILE_KEY, id2);
|
|
17836
|
+
emitStorageChange("active-profile");
|
|
17837
|
+
}
|
|
17838
|
+
function getActiveProfile() {
|
|
17839
|
+
const profiles = getProfiles();
|
|
17840
|
+
if (profiles.length === 0) return null;
|
|
17841
|
+
const activeId = getActiveProfileId();
|
|
17842
|
+
if (activeId) {
|
|
17843
|
+
const found = profiles.find((p2) => p2.id === activeId);
|
|
17844
|
+
if (found) return found;
|
|
18034
17845
|
}
|
|
18035
|
-
|
|
18036
|
-
|
|
18037
|
-
|
|
18038
|
-
|
|
18039
|
-
|
|
17846
|
+
const sorted = [...profiles].sort(
|
|
17847
|
+
(a, b) => (b.lastUsedAt || b.createdAt || 0) - (a.lastUsedAt || a.createdAt || 0)
|
|
17848
|
+
);
|
|
17849
|
+
return sorted[0] || null;
|
|
17850
|
+
}
|
|
17851
|
+
function updateProfile(id2, patch) {
|
|
17852
|
+
const profiles = getProfiles();
|
|
17853
|
+
const idx = profiles.findIndex((p2) => p2.id === id2);
|
|
17854
|
+
if (idx < 0) return null;
|
|
17855
|
+
const updated = { ...profiles[idx], ...patch, lastEditedAt: Date.now() };
|
|
17856
|
+
profiles[idx] = updated;
|
|
17857
|
+
saveProfiles(profiles);
|
|
17858
|
+
return updated;
|
|
17859
|
+
}
|
|
17860
|
+
function updateProfileMeasurements(id2, measurements, unit = "cm") {
|
|
17861
|
+
return updateProfile(id2, { measurements, measurementsUnit: unit });
|
|
17862
|
+
}
|
|
17863
|
+
function addSizeToHistory(profileId, entry) {
|
|
17864
|
+
const profiles = getProfiles();
|
|
17865
|
+
const idx = profiles.findIndex((p2) => p2.id === profileId);
|
|
17866
|
+
if (idx < 0) return null;
|
|
17867
|
+
const profile = profiles[idx];
|
|
17868
|
+
const history = (profile.sizeHistory || []).filter(
|
|
17869
|
+
(h) => h.productId !== entry.productId
|
|
17870
|
+
);
|
|
17871
|
+
history.unshift(entry);
|
|
17872
|
+
const trimmed = history.slice(0, 50);
|
|
17873
|
+
profiles[idx] = {
|
|
17874
|
+
...profile,
|
|
17875
|
+
sizeHistory: trimmed,
|
|
17876
|
+
lastUsedAt: Date.now()
|
|
17877
|
+
};
|
|
17878
|
+
saveProfiles(profiles);
|
|
17879
|
+
return profiles[idx];
|
|
17880
|
+
}
|
|
17881
|
+
function detectLocale() {
|
|
17882
|
+
if (typeof navigator === "undefined") return "US";
|
|
17883
|
+
const lang = navigator.language || "";
|
|
17884
|
+
const region = lang.split("-")[1]?.toUpperCase();
|
|
17885
|
+
if (region === "GB") return "UK";
|
|
17886
|
+
if (region) return region;
|
|
17887
|
+
const map = {
|
|
17888
|
+
en: "US",
|
|
17889
|
+
ja: "JP",
|
|
17890
|
+
ko: "KR",
|
|
17891
|
+
zh: "CN",
|
|
17892
|
+
fr: "FR",
|
|
17893
|
+
it: "IT",
|
|
17894
|
+
de: "DE",
|
|
17895
|
+
es: "ES",
|
|
17896
|
+
pt: "BR"
|
|
17897
|
+
};
|
|
17898
|
+
return map[lang.split("-")[0].toLowerCase()] || "US";
|
|
17899
|
+
}
|
|
17900
|
+
function getApiKey() {
|
|
17901
|
+
let key = "";
|
|
17902
|
+
try {
|
|
17903
|
+
key = "shopify-proxy";
|
|
17904
|
+
} catch {
|
|
18040
17905
|
}
|
|
18041
|
-
|
|
18042
|
-
|
|
18043
|
-
|
|
18044
|
-
|
|
18045
|
-
|
|
18046
|
-
|
|
17906
|
+
if (!key) throw new PrimeStyleError("Missing NEXT_PUBLIC_PRIMESTYLE_API_KEY", "MISSING_API_KEY");
|
|
17907
|
+
return key;
|
|
17908
|
+
}
|
|
17909
|
+
function getApiUrl(override) {
|
|
17910
|
+
if (override) return override;
|
|
17911
|
+
let envUrl = "";
|
|
17912
|
+
try {
|
|
17913
|
+
envUrl = "";
|
|
17914
|
+
} catch {
|
|
18047
17915
|
}
|
|
18048
|
-
|
|
18049
|
-
|
|
18050
|
-
|
|
17916
|
+
return envUrl || "http://localhost:4000";
|
|
17917
|
+
}
|
|
17918
|
+
let cachedMP = null;
|
|
17919
|
+
const MP_CACHE_TTL_MS = 6e4;
|
|
17920
|
+
function setCachedMediaPipe(landmarks) {
|
|
17921
|
+
cachedMP = { landmarks, storedAt: Date.now() };
|
|
17922
|
+
}
|
|
17923
|
+
function getCachedMediaPipe() {
|
|
17924
|
+
if (!cachedMP) return null;
|
|
17925
|
+
if (Date.now() - cachedMP.storedAt > MP_CACHE_TTL_MS) {
|
|
17926
|
+
cachedMP = null;
|
|
17927
|
+
return null;
|
|
18051
17928
|
}
|
|
18052
|
-
|
|
18053
|
-
|
|
18054
|
-
|
|
18055
|
-
|
|
18056
|
-
|
|
18057
|
-
|
|
18058
|
-
|
|
18059
|
-
|
|
17929
|
+
return cachedMP;
|
|
17930
|
+
}
|
|
17931
|
+
async function recommendForProduct(input) {
|
|
17932
|
+
const log = (...args) => console.log("[ps-sdk:recommend]", ...args);
|
|
17933
|
+
const profile = input.profile ?? getActiveProfile();
|
|
17934
|
+
if (!profile) {
|
|
17935
|
+
log("no active profile — returning null");
|
|
17936
|
+
return null;
|
|
18060
17937
|
}
|
|
18061
|
-
|
|
18062
|
-
|
|
18063
|
-
|
|
18064
|
-
|
|
17938
|
+
log("start", {
|
|
17939
|
+
productId: input.productId,
|
|
17940
|
+
profileId: profile.id,
|
|
17941
|
+
profileName: profile.name,
|
|
17942
|
+
hasMeasurements: !!profile.measurements && Object.keys(profile.measurements || {}).length,
|
|
17943
|
+
measurementsCount: Object.keys(profile.measurements || {}).length
|
|
17944
|
+
});
|
|
17945
|
+
log("cache MISS — calling backend");
|
|
17946
|
+
let apiKey;
|
|
17947
|
+
try {
|
|
17948
|
+
apiKey = input.apiKey ?? getApiKey();
|
|
17949
|
+
} catch (e) {
|
|
17950
|
+
log("no api key — aborting", e);
|
|
17951
|
+
return null;
|
|
18065
17952
|
}
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
/* Desktop layout's product image — hidden on mobile because BasicsStepMobile
|
|
18071
|
-
has its own minimal layout. The other steps (chest/midsection/hips) don't
|
|
18072
|
-
need the giant left-column image on a phone either. */
|
|
18073
|
-
.ps-bp-image { display: none !important; }
|
|
18074
|
-
.ps-bp-wrapper { overflow-y: auto; -webkit-overflow-scrolling: touch; flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
18075
|
-
.ps-bp-layout { flex: 1; min-height: 0; }
|
|
18076
|
-
/* Override desktop justify-content:center so BasicsStepMobile can
|
|
18077
|
-
stretch to the bottom of the modal — its bottom action bar (Next +
|
|
18078
|
-
tabs) needs to land at the bottom edge, not floating in the middle. */
|
|
18079
|
-
.ps-bp-root {
|
|
18080
|
-
justify-content: stretch !important;
|
|
18081
|
-
height: auto !important;
|
|
18082
|
-
max-height: none !important;
|
|
18083
|
-
overflow: visible !important;
|
|
18084
|
-
flex: 1 !important;
|
|
18085
|
-
}
|
|
18086
|
-
.ps-bp-upload-placeholder svg { width: 32px; height: 32px; }
|
|
18087
|
-
.ps-bp-upload-text { font-size: 14px; }
|
|
18088
|
-
.ps-bp-upload-hint { font-size: 11px; }
|
|
18089
|
-
.ps-bp-root { padding: 16px; gap: 14px; overflow: visible; }
|
|
18090
|
-
.ps-bp-dots { gap: 6px; }
|
|
18091
|
-
.ps-bp-dot { width: 6px; height: 6px; }
|
|
18092
|
-
.ps-bp-dot-active { width: 18px; border-radius: 4px; }
|
|
18093
|
-
.ps-bp-title { font-size: 18px; }
|
|
18094
|
-
.ps-bp-subtitle { font-size: 13px; margin-top: -4px; }
|
|
18095
|
-
.ps-bp-system-toggle { gap: 20px; margin: 4px 0; }
|
|
18096
|
-
.ps-bp-system-btn { font-size: 13px; }
|
|
18097
|
-
.ps-bp-inline-row { padding: 14px 0; gap: 12px; }
|
|
18098
|
-
.ps-bp-inline-label { flex: 0 0 85px; font-size: 11px; white-space: nowrap; }
|
|
18099
|
-
.ps-bp-inline-input { font-size: 18px; }
|
|
18100
|
-
.ps-bp-inline-unit { font-size: 12px; }
|
|
18101
|
-
.ps-bp-photo-cta { font-size: 13px; gap: 6px; margin-top: 4px; }
|
|
18102
|
-
.ps-bp-photo-cta svg { width: 16px; height: 16px; }
|
|
18103
|
-
.ps-bp-options { gap: 6px; }
|
|
18104
|
-
.ps-bp-option { border-radius: 10px; padding: 6px 12px 6px 6px; gap: 10px; }
|
|
18105
|
-
.ps-bp-option-icon { width: 70px; height: 70px; }
|
|
18106
|
-
.ps-bp-option-label { font-size: 14px; }
|
|
18107
|
-
.ps-bp-option::after { font-size: 18px; }
|
|
18108
|
-
.ps-bp-option { padding: 14px 8px; border-radius: 10px; gap: 8px; }
|
|
18109
|
-
.ps-bp-option-icon { width: 56px; height: 56px; }
|
|
18110
|
-
.ps-bp-option-label { font-size: 13px; }
|
|
18111
|
-
.ps-bp-error { font-size: 12px; }
|
|
18112
|
-
.ps-bp-shape-section { margin-bottom: 12px; }
|
|
18113
|
-
.ps-bp-shape-label { font-size: 13px; margin-bottom: 6px; }
|
|
18114
|
-
.ps-bp-shape-row { gap: 6px; }
|
|
18115
|
-
.ps-bp-img-card { border-radius: 8px; border-width: 2px; }
|
|
18116
|
-
.ps-bp-img-card-label { font-size: 11px; padding: 4px 2px; }
|
|
18117
|
-
.ps-bp-hover-hint { font-size: 10px; margin-top: 6px; }
|
|
18118
|
-
.ps-bp-single-q .ps-bp-title { font-size: 13px; font-weight: 400; }
|
|
18119
|
-
.ps-bp-photo-upload { padding: 10px 12px; gap: 8px; border-radius: 8px; }
|
|
18120
|
-
.ps-bp-photo-upload-text { font-size: 13px; }
|
|
18121
|
-
.ps-bp-photo-upload-hint { font-size: 10px; }
|
|
18122
|
-
.ps-bp-photo-preview { padding: 8px; gap: 8px; border-radius: 8px; }
|
|
18123
|
-
.ps-bp-photo-thumb { width: 40px; height: 40px; border-radius: 6px; }
|
|
18124
|
-
.ps-bp-photo-status { font-size: 12px; gap: 4px; }
|
|
18125
|
-
.ps-bp-photo-hint { font-size: 10px; }
|
|
18126
|
-
.ps-bp-photo-remove { width: 22px; height: 22px; font-size: 16px; }
|
|
18127
|
-
.ps-bp-nav { padding-top: 6px; }
|
|
18128
|
-
.ps-bp-back-btn { font-size: 14px; }
|
|
18129
|
-
.ps-bp-back-arrow { font-size: 18px; }
|
|
18130
|
-
.ps-bp-next-btn {
|
|
18131
|
-
display: flex; align-items: center; gap: 0.5vw; padding: 10px 16px; font-size: 13px; border-radius: 6px; }
|
|
18132
|
-
.ps-bp-root { padding: 12px; overflow: hidden; }
|
|
18133
|
-
/* Accessory sizing inputs — mobile */
|
|
18134
|
-
.ps-acc-input { padding: 3vw; font-size: 3.6vw; border-radius: 2vw; }
|
|
18135
|
-
.ps-acc-label { font-size: 2.8vw; margin-bottom: 1.5vw; }
|
|
18136
|
-
.ps-acc-field { margin: 3vw 0; }
|
|
18137
|
-
.ps-acc-error { font-size: 2.8vw; }
|
|
18138
|
-
.ps-acc-hint { font-size: 2.5vw; }
|
|
18139
|
-
.ps-tryon-unit-toggle { border-radius: 2vw; }
|
|
18140
|
-
.ps-tryon-unit-btn { padding: 2vw 4vw; font-size: 3vw; }
|
|
17953
|
+
const apiUrl = (input.apiUrl ?? getApiUrl()).replace(/\/+$/, "");
|
|
17954
|
+
if (!apiKey) {
|
|
17955
|
+
log("no api key — aborting");
|
|
17956
|
+
return null;
|
|
18141
17957
|
}
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
17958
|
+
let sizeGuide = null;
|
|
17959
|
+
if (input.sizeGuideData != null) {
|
|
17960
|
+
try {
|
|
17961
|
+
const sgRes = await fetch(`${apiUrl}/api/v1/sizing/sizeguide`, {
|
|
17962
|
+
method: "POST",
|
|
17963
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
17964
|
+
body: JSON.stringify({
|
|
17965
|
+
product: { title: input.productTitle, productId: input.productId },
|
|
17966
|
+
sizeGuideRaw: input.sizeGuideData
|
|
17967
|
+
})
|
|
17968
|
+
});
|
|
17969
|
+
if (sgRes.ok) {
|
|
17970
|
+
sizeGuide = await sgRes.json();
|
|
17971
|
+
log("sizeguide OK", { found: sizeGuide?.found, sectionCount: Object.keys(sizeGuide?.sections || {}).length });
|
|
17972
|
+
} else {
|
|
17973
|
+
log("sizeguide FAILED", sgRes.status, sgRes.statusText);
|
|
17974
|
+
}
|
|
17975
|
+
} catch (e) {
|
|
17976
|
+
log("sizeguide threw", e);
|
|
17977
|
+
}
|
|
17978
|
+
} else {
|
|
17979
|
+
log("no sizeGuideData provided");
|
|
18155
17980
|
}
|
|
18156
|
-
|
|
18157
|
-
|
|
18158
|
-
|
|
18159
|
-
|
|
18160
|
-
|
|
17981
|
+
const measurements = {
|
|
17982
|
+
gender: profile.gender,
|
|
17983
|
+
sizingUnit: profile.measurementsUnit || "cm"
|
|
17984
|
+
};
|
|
17985
|
+
if (profile.measurements) {
|
|
17986
|
+
for (const [key, value] of Object.entries(profile.measurements)) {
|
|
17987
|
+
if (value != null) measurements[key] = value;
|
|
17988
|
+
}
|
|
18161
17989
|
}
|
|
18162
|
-
.
|
|
18163
|
-
.
|
|
18164
|
-
|
|
18165
|
-
.
|
|
18166
|
-
|
|
18167
|
-
|
|
17990
|
+
if (profile.height != null) measurements.height = profile.height;
|
|
17991
|
+
if (profile.weight != null) measurements.weight = profile.weight;
|
|
17992
|
+
if (profile.heightUnit) measurements.heightUnit = profile.heightUnit;
|
|
17993
|
+
if (profile.weightUnit) measurements.weightUnit = profile.weightUnit;
|
|
17994
|
+
if (profile.age) measurements.age = profile.age;
|
|
17995
|
+
if (profile.chestProfile) measurements.chestProfile = profile.chestProfile;
|
|
17996
|
+
if (profile.midsectionProfile) measurements.midsectionProfile = profile.midsectionProfile;
|
|
17997
|
+
if (profile.hipProfile) measurements.hipProfile = profile.hipProfile;
|
|
17998
|
+
const payload = {
|
|
17999
|
+
method: "exact",
|
|
18000
|
+
locale: profile.country || "US",
|
|
18001
|
+
sizingUnit: profile.measurementsUnit || profile.sizingUnit || "cm",
|
|
18002
|
+
product: { title: input.productTitle, productId: input.productId, description: "", variants: [] },
|
|
18003
|
+
measurements
|
|
18004
|
+
};
|
|
18005
|
+
if (profile.gender === "female" && profile.bandSize && profile.cupSize) {
|
|
18006
|
+
const region = profile.braRegion ?? profile.braSizeRegion ?? "US";
|
|
18007
|
+
payload.braSize = {
|
|
18008
|
+
band: parseFloat(String(profile.bandSize)),
|
|
18009
|
+
cup: String(profile.cupSize),
|
|
18010
|
+
region
|
|
18011
|
+
};
|
|
18168
18012
|
}
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18172
|
-
.ps-photo-zone-img {
|
|
18173
|
-
width: 100%; height: 100%; object-fit: contain;
|
|
18174
|
-
display: block; cursor: pointer;
|
|
18013
|
+
if (profile.knownMeasurements) {
|
|
18014
|
+
payload.knownMeasurements = profile.knownMeasurements;
|
|
18175
18015
|
}
|
|
18176
|
-
.
|
|
18177
|
-
|
|
18178
|
-
display: flex; align-items: center; justify-content: center;
|
|
18179
|
-
background: rgba(0,0,0,0.4); opacity: 0; transition: opacity 0.2s;
|
|
18180
|
-
color: #fff; font-size: 0.8vw; font-weight: 600;
|
|
18181
|
-
border-radius: 0.5vw; cursor: pointer;
|
|
18016
|
+
if (sizeGuide && sizeGuide.found) {
|
|
18017
|
+
payload.sizeGuide = sizeGuide;
|
|
18182
18018
|
}
|
|
18183
|
-
|
|
18184
|
-
|
|
18185
|
-
|
|
18186
|
-
|
|
18187
|
-
|
|
18188
|
-
|
|
18189
|
-
|
|
18190
|
-
|
|
18019
|
+
log("calling /sizing/recommend", { measurements: Object.keys(measurements), hasSizeGuide: !!payload.sizeGuide });
|
|
18020
|
+
let result = null;
|
|
18021
|
+
try {
|
|
18022
|
+
const res = await fetch(`${apiUrl}/api/v1/sizing/recommend`, {
|
|
18023
|
+
method: "POST",
|
|
18024
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
18025
|
+
body: JSON.stringify(payload)
|
|
18026
|
+
});
|
|
18027
|
+
if (!res.ok) {
|
|
18028
|
+
log("recommend FAILED", res.status, res.statusText);
|
|
18029
|
+
return null;
|
|
18030
|
+
}
|
|
18031
|
+
result = await res.json();
|
|
18032
|
+
log("recommend OK", {
|
|
18033
|
+
recommendedSize: result?.recommendedSize,
|
|
18034
|
+
sectionKeys: result?.sections ? Object.keys(result.sections) : null,
|
|
18035
|
+
sections: result?.sections
|
|
18036
|
+
});
|
|
18037
|
+
} catch (e) {
|
|
18038
|
+
log("recommend threw", e);
|
|
18039
|
+
return null;
|
|
18191
18040
|
}
|
|
18192
|
-
|
|
18193
|
-
|
|
18194
|
-
|
|
18195
|
-
display: flex; flex-direction: column; align-items: center; gap: 0.5vw;
|
|
18196
|
-
padding: 1vw;
|
|
18041
|
+
if (!result || !result.recommendedSize) {
|
|
18042
|
+
log("recommend returned no recommendedSize — bailing");
|
|
18043
|
+
return null;
|
|
18197
18044
|
}
|
|
18198
|
-
.
|
|
18199
|
-
|
|
18200
|
-
|
|
18201
|
-
|
|
18202
|
-
|
|
18203
|
-
|
|
18045
|
+
const sectionsMap = result.sections ? Object.fromEntries(
|
|
18046
|
+
Object.entries(result.sections).map(([name, sec]) => [name, sec.recommendedSize])
|
|
18047
|
+
) : void 0;
|
|
18048
|
+
const sectionsFull = result.sections ? Object.fromEntries(
|
|
18049
|
+
Object.entries(result.sections).map(([name, sec]) => [
|
|
18050
|
+
name,
|
|
18051
|
+
{
|
|
18052
|
+
recommendedSize: sec.recommendedSize,
|
|
18053
|
+
// Backend may include these on merged Jacket/Pants entries
|
|
18054
|
+
size: sec.size,
|
|
18055
|
+
length: sec.length,
|
|
18056
|
+
availableSizes: sec.availableSizes,
|
|
18057
|
+
availableLengths: sec.availableLengths
|
|
18058
|
+
}
|
|
18059
|
+
])
|
|
18060
|
+
) : void 0;
|
|
18061
|
+
addSizeToHistory(profile.id, {
|
|
18062
|
+
productId: input.productId,
|
|
18063
|
+
productTitle: input.productTitle,
|
|
18064
|
+
productImage: input.productImage,
|
|
18065
|
+
recommendedSize: result.recommendedSize,
|
|
18066
|
+
confidence: result.confidence,
|
|
18067
|
+
sections: sectionsMap,
|
|
18068
|
+
sectionsFull,
|
|
18069
|
+
recommendedLength: result.recommendedLength || void 0,
|
|
18070
|
+
savedAt: Date.now()
|
|
18071
|
+
});
|
|
18072
|
+
return {
|
|
18073
|
+
recommendedSize: result.recommendedSize,
|
|
18074
|
+
confidence: result.confidence,
|
|
18075
|
+
sections: sectionsMap,
|
|
18076
|
+
profileId: profile.id,
|
|
18077
|
+
fromCache: false,
|
|
18078
|
+
raw: result,
|
|
18079
|
+
found: result.found
|
|
18080
|
+
};
|
|
18081
|
+
}
|
|
18082
|
+
async function estimateFullMeasurements(args) {
|
|
18083
|
+
let apiKey;
|
|
18084
|
+
try {
|
|
18085
|
+
apiKey = args.apiKey ?? getApiKey();
|
|
18086
|
+
} catch {
|
|
18087
|
+
return null;
|
|
18204
18088
|
}
|
|
18205
|
-
|
|
18206
|
-
|
|
18207
|
-
|
|
18208
|
-
|
|
18209
|
-
|
|
18210
|
-
|
|
18089
|
+
const apiUrl = (args.apiUrl ?? getApiUrl()).replace(/\/+$/, "");
|
|
18090
|
+
if (!apiKey) return null;
|
|
18091
|
+
const requiredFields = [
|
|
18092
|
+
"chest",
|
|
18093
|
+
"bust",
|
|
18094
|
+
"waist",
|
|
18095
|
+
"hips",
|
|
18096
|
+
"shoulderWidth",
|
|
18097
|
+
"sleeveLength",
|
|
18098
|
+
"inseam",
|
|
18099
|
+
"neckCircumference",
|
|
18100
|
+
"thighCircumference",
|
|
18101
|
+
"footLengthCm"
|
|
18102
|
+
];
|
|
18103
|
+
const payload = {
|
|
18104
|
+
height: args.height ?? 0,
|
|
18105
|
+
weight: args.weight ?? 0,
|
|
18106
|
+
heightUnit: args.heightUnit,
|
|
18107
|
+
weightUnit: args.weightUnit,
|
|
18108
|
+
gender: args.gender,
|
|
18109
|
+
requiredFields
|
|
18110
|
+
};
|
|
18111
|
+
if (args.age) payload.age = args.age;
|
|
18112
|
+
if (args.chestProfile) payload.chestProfile = args.chestProfile;
|
|
18113
|
+
if (args.midsectionProfile) payload.midsectionProfile = args.midsectionProfile;
|
|
18114
|
+
if (args.hipProfile) payload.hipProfile = args.hipProfile;
|
|
18115
|
+
if (args.bodyImage) payload.bodyImage = args.bodyImage;
|
|
18116
|
+
let landmarksForPayload = args.bodyLandmarks ?? null;
|
|
18117
|
+
if (!landmarksForPayload) {
|
|
18118
|
+
const cached = getCachedMediaPipe();
|
|
18119
|
+
if (cached) {
|
|
18120
|
+
landmarksForPayload = cached.landmarks;
|
|
18121
|
+
console.log("[PS-SDK:estimate] reusing cached MediaPipe result from recent /recommend run");
|
|
18122
|
+
}
|
|
18211
18123
|
}
|
|
18212
|
-
.
|
|
18213
|
-
|
|
18214
|
-
|
|
18215
|
-
|
|
18216
|
-
|
|
18124
|
+
if (!landmarksForPayload && args.bodyImage) {
|
|
18125
|
+
console.log("[PS-SDK:estimate] bodyImage present but bodyLandmarks missing — running MediaPipe now");
|
|
18126
|
+
try {
|
|
18127
|
+
const detected = await detectBodyLandmarks(args.bodyImage);
|
|
18128
|
+
if (detected) {
|
|
18129
|
+
landmarksForPayload = detected;
|
|
18130
|
+
console.log("[PS-SDK:estimate] MediaPipe auto-detect ✓ anchors available for backend");
|
|
18131
|
+
} else {
|
|
18132
|
+
console.warn("[PS-SDK:estimate] MediaPipe auto-detect returned null — backend will fall back to low-confidence visual estimation");
|
|
18133
|
+
}
|
|
18134
|
+
} catch (e) {
|
|
18135
|
+
console.warn("[PS-SDK:estimate] MediaPipe auto-detect threw:", e);
|
|
18136
|
+
}
|
|
18217
18137
|
}
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
|
|
18224
|
-
|
|
18225
|
-
|
|
18138
|
+
if (landmarksForPayload) payload.bodyLandmarks = landmarksForPayload;
|
|
18139
|
+
if (landmarksForPayload) {
|
|
18140
|
+
const lm = landmarksForPayload;
|
|
18141
|
+
const required = [
|
|
18142
|
+
["leftShoulder", lm.leftShoulder?.visibility],
|
|
18143
|
+
["rightShoulder", lm.rightShoulder?.visibility],
|
|
18144
|
+
["leftHip", lm.leftHip?.visibility],
|
|
18145
|
+
["rightHip", lm.rightHip?.visibility]
|
|
18146
|
+
];
|
|
18147
|
+
const lowVis = required.filter(([, v2]) => typeof v2 === "number" && v2 < 0.5);
|
|
18148
|
+
if (lowVis.length > 0) {
|
|
18149
|
+
console.warn(`[PS-SDK:estimate] LOW joint visibility — ${lowVis.map(([k2, v2]) => `${k2}=${v2.toFixed(2)}`).join(", ")} — accuracy may suffer; ask user to retake with better framing/lighting`);
|
|
18150
|
+
}
|
|
18226
18151
|
}
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18152
|
+
try {
|
|
18153
|
+
console.groupCollapsed(`[PS-SDK:estimate] POST /api/v1/sizing/estimate`);
|
|
18154
|
+
console.log("url:", `${apiUrl}/api/v1/sizing/estimate`);
|
|
18155
|
+
console.log("payload (sans bodyImage):", {
|
|
18156
|
+
...payload,
|
|
18157
|
+
bodyImage: payload.bodyImage ? `[base64 ~${Math.round(String(payload.bodyImage).length / 1024)}KB]` : void 0,
|
|
18158
|
+
bodyLandmarks: payload.bodyLandmarks ? {
|
|
18159
|
+
count: Object.keys(payload.bodyLandmarks).length,
|
|
18160
|
+
imageWidth: payload.bodyLandmarks.imageWidth,
|
|
18161
|
+
imageHeight: payload.bodyLandmarks.imageHeight
|
|
18162
|
+
} : void 0
|
|
18163
|
+
});
|
|
18164
|
+
console.groupEnd();
|
|
18165
|
+
{
|
|
18166
|
+
const lmObj = payload.bodyLandmarks;
|
|
18167
|
+
const jointCount = lmObj ? Object.values(lmObj).filter(
|
|
18168
|
+
(v2) => v2 && typeof v2 === "object" && typeof v2.x === "number" && typeof v2.y === "number"
|
|
18169
|
+
).length : 0;
|
|
18170
|
+
console.log(`[ps-sdk:debug] /estimate payload → bodyLandmarks=${!!lmObj}(${jointCount} joints)`);
|
|
18171
|
+
}
|
|
18172
|
+
const res = await fetch(`${apiUrl}/api/v1/sizing/estimate`, {
|
|
18173
|
+
method: "POST",
|
|
18174
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
18175
|
+
body: JSON.stringify(payload)
|
|
18176
|
+
});
|
|
18177
|
+
if (!res.ok) {
|
|
18178
|
+
console.warn(`[PS-SDK:estimate] ✗ ${res.status} ${res.statusText}`);
|
|
18179
|
+
return null;
|
|
18180
|
+
}
|
|
18181
|
+
const data = await res.json();
|
|
18182
|
+
console.groupCollapsed(`[PS-SDK:estimate] ✓ response (confidence=${data?.confidence ?? "—"})`);
|
|
18183
|
+
console.log("estimates:", data.estimates);
|
|
18184
|
+
console.log("userEstimates:", data.userEstimates);
|
|
18185
|
+
console.log("method:", data.method, "unit:", data.unit);
|
|
18186
|
+
console.log("full response:", data);
|
|
18187
|
+
console.groupEnd();
|
|
18188
|
+
if (!data?.estimates) return null;
|
|
18189
|
+
return {
|
|
18190
|
+
estimates: data.estimates,
|
|
18191
|
+
unit: data.unit || "cm",
|
|
18192
|
+
userEstimates: data.userEstimates
|
|
18193
|
+
};
|
|
18194
|
+
} catch (err) {
|
|
18195
|
+
console.error("[PS-SDK:estimate] threw:", err);
|
|
18196
|
+
return null;
|
|
18235
18197
|
}
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18198
|
+
}
|
|
18199
|
+
function isImperial(locale) {
|
|
18200
|
+
return ["US", "UK", "AU"].includes(locale);
|
|
18201
|
+
}
|
|
18202
|
+
function getUnitLabel(unit) {
|
|
18203
|
+
if (unit === "in" || unit === "inches" || unit === "lbs") return "Imperial";
|
|
18204
|
+
if (unit === "cm" || unit === "kg") return "Metric";
|
|
18205
|
+
if (unit === "mm") return "mm";
|
|
18206
|
+
return "";
|
|
18207
|
+
}
|
|
18208
|
+
const cache = /* @__PURE__ */ new Map();
|
|
18209
|
+
function scoreLandmarks(lm) {
|
|
18210
|
+
if (!lm) return 0;
|
|
18211
|
+
let joints = 0;
|
|
18212
|
+
for (const [k2, v2] of Object.entries(lm)) {
|
|
18213
|
+
if (k2 === "imageWidth" || k2 === "imageHeight") continue;
|
|
18214
|
+
if (v2 && typeof v2 === "object" && typeof v2.x === "number") joints++;
|
|
18248
18215
|
}
|
|
18249
|
-
|
|
18216
|
+
let score = joints * 10;
|
|
18217
|
+
if (lm.nose) score += 50;
|
|
18218
|
+
if (lm.leftAnkle && lm.rightAnkle) score += 5;
|
|
18219
|
+
return score;
|
|
18220
|
+
}
|
|
18221
|
+
async function scoreImage(url) {
|
|
18222
|
+
try {
|
|
18223
|
+
const lm = await detectBodyLandmarks(url);
|
|
18224
|
+
return scoreLandmarks(lm);
|
|
18225
|
+
} catch {
|
|
18226
|
+
return 0;
|
|
18227
|
+
}
|
|
18228
|
+
}
|
|
18229
|
+
async function pickBestGarmentImage(images) {
|
|
18230
|
+
if (!images || !images.length) return null;
|
|
18231
|
+
if (images.length === 1) return images[0];
|
|
18232
|
+
const cacheKey = images.join("|");
|
|
18233
|
+
const cached = cache.get(cacheKey);
|
|
18234
|
+
if (cached) return cached;
|
|
18235
|
+
const t0 = Date.now();
|
|
18236
|
+
const scored = await Promise.all(images.map(async (url) => ({
|
|
18237
|
+
url,
|
|
18238
|
+
score: await scoreImage(url)
|
|
18239
|
+
})));
|
|
18240
|
+
scored.sort((a, b) => b.score - a.score);
|
|
18241
|
+
const best = (scored[0]?.score ?? 0) > 0 ? scored[0].url : images[0];
|
|
18242
|
+
cache.set(cacheKey, best);
|
|
18243
|
+
console.log(`[ps-sdk:garment-pick] ${Date.now() - t0}ms — chose ${images.indexOf(best)}/${images.length}`);
|
|
18244
|
+
for (const s of scored) console.log(`[ps-sdk:garment-pick] ${s.score.toString().padStart(4, " ")} ${s.url}`);
|
|
18245
|
+
return best;
|
|
18246
|
+
}
|
|
18247
|
+
function cx(base, override) {
|
|
18248
|
+
return override ? `${base} ${override}` : base;
|
|
18249
|
+
}
|
|
18250
18250
|
function CameraIcon$1({ size = 18 }) {
|
|
18251
18251
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
18252
18252
|
/* @__PURE__ */ jsxRuntimeExports.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" }),
|
|
@@ -27589,12 +27589,15 @@ function FaceSizeView(props) {
|
|
|
27589
27589
|
);
|
|
27590
27590
|
}
|
|
27591
27591
|
if (typeof document !== "undefined") {
|
|
27592
|
-
const
|
|
27593
|
-
if (!
|
|
27594
|
-
const
|
|
27595
|
-
|
|
27596
|
-
|
|
27597
|
-
|
|
27592
|
+
const skipForShadow = !!globalThis.__PRIMESTYLE_SHADOW_ACTIVE__;
|
|
27593
|
+
if (!skipForShadow) {
|
|
27594
|
+
const id2 = "ps-tryon-styles";
|
|
27595
|
+
if (!document.getElementById(id2)) {
|
|
27596
|
+
const el2 = document.createElement("style");
|
|
27597
|
+
el2.id = id2;
|
|
27598
|
+
el2.textContent = STYLES$1;
|
|
27599
|
+
document.head.appendChild(el2);
|
|
27600
|
+
}
|
|
27598
27601
|
}
|
|
27599
27602
|
}
|
|
27600
27603
|
function detectMeasurementType(title) {
|
|
@@ -29832,9 +29835,14 @@ const STYLES = `
|
|
|
29832
29835
|
color: #1f2937;
|
|
29833
29836
|
}
|
|
29834
29837
|
`;
|
|
29838
|
+
const SIZE_GUIDE_STYLES = STYLES;
|
|
29835
29839
|
let stylesInjected = false;
|
|
29836
29840
|
function injectStyles() {
|
|
29837
29841
|
if (stylesInjected) return;
|
|
29842
|
+
if (globalThis.__PRIMESTYLE_SHADOW_ACTIVE__) {
|
|
29843
|
+
stylesInjected = true;
|
|
29844
|
+
return;
|
|
29845
|
+
}
|
|
29838
29846
|
const tag = document.createElement("style");
|
|
29839
29847
|
tag.id = "ps-sg-styles";
|
|
29840
29848
|
tag.textContent = STYLES;
|
|
@@ -30203,6 +30211,8 @@ function installProductViewTracking() {
|
|
|
30203
30211
|
const roots = document.querySelectorAll(".primestyle-tryon-root");
|
|
30204
30212
|
roots.forEach((r2) => maybeFireProductView(r2));
|
|
30205
30213
|
}
|
|
30214
|
+
globalThis.__PRIMESTYLE_SHADOW_ACTIVE__ = true;
|
|
30215
|
+
const SDK_CSS = STYLES$1 + "\n\n" + SIZE_GUIDE_STYLES;
|
|
30206
30216
|
const TAG = "[primestyle-tryon]";
|
|
30207
30217
|
console.log(`${TAG} bundle loaded — version 5.7.x storefront entry`);
|
|
30208
30218
|
installCartHook();
|
|
@@ -30291,10 +30301,35 @@ async function mount(el2) {
|
|
|
30291
30301
|
props.sizeGuideData = fetched;
|
|
30292
30302
|
}
|
|
30293
30303
|
}
|
|
30304
|
+
let mountTarget = el2;
|
|
30305
|
+
try {
|
|
30306
|
+
const shadow = el2.shadowRoot ?? el2.attachShadow({ mode: "open" });
|
|
30307
|
+
if (!shadow.querySelector("style[data-primestyle]")) {
|
|
30308
|
+
const style = document.createElement("style");
|
|
30309
|
+
style.setAttribute("data-primestyle", "sdk-styles");
|
|
30310
|
+
style.textContent = SDK_CSS;
|
|
30311
|
+
shadow.appendChild(style);
|
|
30312
|
+
}
|
|
30313
|
+
let inner = shadow.querySelector(
|
|
30314
|
+
"[data-primestyle-mount]"
|
|
30315
|
+
);
|
|
30316
|
+
if (!inner) {
|
|
30317
|
+
inner = document.createElement("div");
|
|
30318
|
+
inner.setAttribute("data-primestyle-mount", "");
|
|
30319
|
+
shadow.appendChild(inner);
|
|
30320
|
+
}
|
|
30321
|
+
mountTarget = inner;
|
|
30322
|
+
} catch (err) {
|
|
30323
|
+
console.warn(
|
|
30324
|
+
`${TAG} shadow attach failed — falling back to direct mount`,
|
|
30325
|
+
err
|
|
30326
|
+
);
|
|
30327
|
+
mountTarget = el2;
|
|
30328
|
+
}
|
|
30294
30329
|
if (props.sizeGuideData) {
|
|
30295
30330
|
try {
|
|
30296
30331
|
const buttonStyles = props.buttonStyles;
|
|
30297
|
-
createSizeGuideButton(
|
|
30332
|
+
createSizeGuideButton(mountTarget, props.sizeGuideData, {
|
|
30298
30333
|
accentColor: buttonStyles?.backgroundColor
|
|
30299
30334
|
});
|
|
30300
30335
|
console.log(`${TAG} ✓ size guide button mounted`);
|
|
@@ -30303,7 +30338,7 @@ async function mount(el2) {
|
|
|
30303
30338
|
}
|
|
30304
30339
|
}
|
|
30305
30340
|
try {
|
|
30306
|
-
const root = createRoot(
|
|
30341
|
+
const root = createRoot(mountTarget);
|
|
30307
30342
|
root.render(reactExports.createElement(PrimeStyleTryon, props));
|
|
30308
30343
|
MOUNTED.set(el2, root);
|
|
30309
30344
|
console.log(`${TAG} ✓ mounted React component`);
|