@primestyleai/tryon 5.10.178 → 5.10.179

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.
@@ -228,6 +228,8 @@ export interface PrimeStyleTryonProps {
228
228
  * the fabric correctly without guessing from the image alone. */
229
229
  productMaterial?: string;
230
230
  buttonText?: string;
231
+ /** When true, a completed try-on for this product turns the storefront button into a saved fit result. */
232
+ limitOneColorTryOnPerProduct?: boolean;
231
233
  apiUrl?: string;
232
234
  showPoweredBy?: boolean;
233
235
  /** Show the default camera icon in the button. Defaults to true. Set to false to hide it. */
@@ -24,9 +24,9 @@ export declare function buildFitInfo(matchDetails: Array<{
24
24
  }>, poseLines?: MeasurementLines | null, unit?: "in" | "cm" | "mm"): FitAreaInfo[];
25
25
  /**
26
26
  * Build silhouette-prompt context forwarded to the backend try-on Gemini
27
- * call. Returns the three text-form inputs that the doc example fed to
28
- * Gemini (user raw measurements, recommended size, chart summary) so the
29
- * try-on prompt can reason about absolute values and the chart's grading.
27
+ * call. Returns only the selected fit context Gemini needs: user raw
28
+ * measurements, recommended size, and the chosen size's measurement row.
29
+ * The full product size guide is intentionally not forwarded to try-on.
30
30
  *
31
31
  * All fields optional — returns `undefined` when there's nothing useful
32
32
  * to add (e.g. accessory flow, no sizingResult).
@@ -70,7 +70,6 @@ selectedSizeOverride?: string,
70
70
  userHeight?: string, userWeight?: string): {
71
71
  recommendedSize?: string;
72
72
  recommendedSizeMeasurements?: string;
73
- sizeChartSummary?: string;
74
73
  userMeasurementsText?: string;
75
74
  userHeight?: string;
76
75
  userWeight?: string;
@@ -10225,6 +10225,52 @@ function parseNum(s) {
10225
10225
  const n2 = parseFloat(s.replace(/[^\d.]/g, ""));
10226
10226
  return isNaN(n2) ? 0 : n2;
10227
10227
  }
10228
+ function normalizeUnit(unit) {
10229
+ const u2 = (unit || "").toLowerCase();
10230
+ if (u2 === "in" || u2 === "inch" || u2 === "inches") return "in";
10231
+ if (u2 === "cm") return "cm";
10232
+ if (u2 === "mm") return "mm";
10233
+ return null;
10234
+ }
10235
+ function detectUnitFromText(text) {
10236
+ const t2 = String(text || "");
10237
+ if (/\bmm\b/i.test(t2)) return "mm";
10238
+ if (/\bcm\b/i.test(t2)) return "cm";
10239
+ if (/\bin\b|inch|inches|["”]/i.test(t2)) return "in";
10240
+ return null;
10241
+ }
10242
+ function convertValue(value, from, to) {
10243
+ if (from === to) return value;
10244
+ const inMm = from === "mm" ? value : from === "cm" ? value * 10 : value * 25.4;
10245
+ if (to === "mm") return inMm;
10246
+ if (to === "cm") return inMm / 10;
10247
+ return inMm / 25.4;
10248
+ }
10249
+ function formatUnitNumber(value, unit) {
10250
+ const precision = unit === "mm" ? 0 : 1;
10251
+ const rounded = Number(value.toFixed(precision));
10252
+ return Number.isInteger(rounded) ? String(rounded) : String(rounded);
10253
+ }
10254
+ function convertNumberTextToUnit(text, from, to) {
10255
+ if (from === to) return text;
10256
+ const converted = text.replace(/\s*(cm|mm|in|inch|inches)\b/ig, "").replace(/(\d+(?:\.\d+)?)/g, (raw) => formatUnitNumber(convertValue(Number(raw), from, to), to)).trim();
10257
+ return converted ? `${converted} ${to}` : text;
10258
+ }
10259
+ function normalizeMeasurementValueText(measurement, value, targetUnit) {
10260
+ if (isUnitlessShoeSizeMeasurement(measurement)) return formatUserMeasurementValue(measurement, value);
10261
+ const sourceUnit = detectUnitFromText(value);
10262
+ if (sourceUnit) return convertNumberTextToUnit(value, sourceUnit, targetUnit);
10263
+ const trimmed = String(value || "").trim();
10264
+ return /\d/.test(trimmed) ? `${trimmed} ${targetUnit}` : trimmed;
10265
+ }
10266
+ function normalizeRangeText(range, targetUnit, header) {
10267
+ const sourceUnit = detectUnitFromText(range) || detectUnitFromText(header);
10268
+ if (!sourceUnit) return String(range || "").trim();
10269
+ return convertNumberTextToUnit(range, sourceUnit, targetUnit);
10270
+ }
10271
+ function promptHeaderLabel(header) {
10272
+ return String(header || "").replace(/\s*\((?:cm|mm|in|inch|inches)\)\s*/ig, "").trim();
10273
+ }
10228
10274
  function isUnitlessShoeSizeMeasurement(measurement) {
10229
10275
  const normalized = measurement.toLowerCase().replace(/\s*\(.*?\)\s*/g, " ").trim();
10230
10276
  if (/foot\s*length|length\s*\(cm\)|cm\s*\/\s*jp|mondo|mondopoint/i.test(normalized)) return false;
@@ -10235,11 +10281,15 @@ function formatUserMeasurementValue(measurement, value) {
10235
10281
  return value.replace(/\s*(cm|mm|in|inch|inches)\b/ig, "").trim();
10236
10282
  }
10237
10283
  function computeFit(userValue, chartRange, unit) {
10238
- const { min: rMin, max: rMax } = parseRange(chartRange);
10284
+ const targetUnit = normalizeUnit(unit) || detectUnitFromText(chartRange) || "in";
10285
+ const chartUnit = detectUnitFromText(chartRange);
10286
+ const parsed = parseRange(chartRange);
10287
+ const rMin = chartUnit && chartUnit !== targetUnit ? convertValue(parsed.min, chartUnit, targetUnit) : parsed.min;
10288
+ const rMax = chartUnit && chartUnit !== targetUnit ? convertValue(parsed.max, chartUnit, targetUnit) : parsed.max;
10239
10289
  if (rMin === 0 && rMax === 0) return "good";
10240
- const perfectTol = unit === "cm" ? 1.27 : unit === "mm" ? 12.7 : 0.5;
10241
- const aBitTol = unit === "cm" ? 2.54 : unit === "mm" ? 25.4 : 1;
10242
- const tooFarTol = unit === "cm" ? 5.08 : unit === "mm" ? 50.8 : 2;
10290
+ const perfectTol = targetUnit === "cm" ? 1.27 : targetUnit === "mm" ? 12.7 : 0.5;
10291
+ const aBitTol = targetUnit === "cm" ? 2.54 : targetUnit === "mm" ? 25.4 : 1;
10292
+ const tooFarTol = targetUnit === "cm" ? 5.08 : targetUnit === "mm" ? 50.8 : 2;
10243
10293
  const inRange = userValue >= rMin && userValue <= rMax;
10244
10294
  const overEdge = inRange ? 0 : userValue > rMax ? userValue - rMax : rMin - userValue;
10245
10295
  if (inRange || overEdge <= perfectTol) return "good";
@@ -10262,14 +10312,17 @@ const SKIP_AREAS_FOR_FIT = /* @__PURE__ */ new Set([
10262
10312
  ]);
10263
10313
  function buildFitInfo(matchDetails, poseLines, unit) {
10264
10314
  return matchDetails.filter((m2) => !SKIP_AREAS_FOR_FIT.has(m2.measurement.toLowerCase().replace(/\s*\(.*?\)\s*/g, "").trim())).map((m2) => {
10265
- const userNum = parseNum(m2.userValue);
10266
- const fit = computeFit(userNum, m2.chartRange, unit);
10315
+ const targetUnit = normalizeUnit(unit) || detectUnitFromText(m2.userValue) || detectUnitFromText(m2.chartRange) || "in";
10316
+ const userValueText = normalizeMeasurementValueText(m2.measurement, m2.userValue, targetUnit);
10317
+ const chartRangeText = normalizeRangeText(m2.chartRange, targetUnit);
10318
+ const userNum = parseNum(userValueText);
10319
+ const fit = computeFit(userNum, chartRangeText, targetUnit);
10267
10320
  const info = {
10268
10321
  area: m2.measurement,
10269
10322
  section: m2.section || void 0,
10270
10323
  fit,
10271
10324
  userValue: userNum || void 0,
10272
- garmentRange: m2.chartRange || void 0
10325
+ garmentRange: chartRangeText || void 0
10273
10326
  };
10274
10327
  if (poseLines) {
10275
10328
  const poseKey = AREA_TO_POSE_KEY[m2.measurement.toLowerCase()];
@@ -10286,6 +10339,7 @@ function buildFitInfo(matchDetails, poseLines, unit) {
10286
10339
  function buildSilhouetteContext(sizingResult, sizeGuide, selectedSizeOverride, userHeight, userWeight) {
10287
10340
  if (!sizingResult && !sizeGuide && !userHeight && !userWeight) return void 0;
10288
10341
  const out = {};
10342
+ const promptUnit = normalizeUnit(sizingResult?.unit) || detectUnitFromText(sizingResult?.matchDetails?.[0]?.userValue) || detectUnitFromText(Object.values(sizingResult?.sections || {})[0]?.matchDetails?.[0]?.userValue) || "in";
10289
10343
  const baseSize = (selectedSizeOverride || sizingResult?.recommendedSize || "").toString().trim();
10290
10344
  if (userHeight) out.userHeight = userHeight;
10291
10345
  if (userWeight) out.userWeight = userWeight;
@@ -10311,7 +10365,7 @@ function buildSilhouetteContext(sizingResult, sizeGuide, selectedSizeOverride, u
10311
10365
  chartRowFit = vStr;
10312
10366
  continue;
10313
10367
  }
10314
- if (vStr) bits.push(`${h} ${vStr}`);
10368
+ if (vStr) bits.push(`${promptHeaderLabel(h) || h} ${normalizeRangeText(vStr, promptUnit, h)}`);
10315
10369
  if (chartRowLength == null && /^length\b|inseam/i.test(h) && vStr) {
10316
10370
  chartRowLength = vStr;
10317
10371
  }
@@ -10360,7 +10414,7 @@ function buildSilhouetteContext(sizingResult, sizeGuide, selectedSizeOverride, u
10360
10414
  const h = (secChart.headers[i] || "").trim();
10361
10415
  const baseHeader = h.replace(/\s*\(.*?\)\s*/g, "").trim();
10362
10416
  if (vStr && !/^(size|standard|country|fit|silhouette|category|body[\s_]?type|eu|uk|us|it|jp|cn|kr|ru|br|au)$/i.test(baseHeader)) {
10363
- bits.push(`${h} ${vStr}`);
10417
+ bits.push(`${promptHeaderLabel(h) || h} ${normalizeRangeText(vStr, promptUnit, h)}`);
10364
10418
  }
10365
10419
  }
10366
10420
  if (bits.length) measurementParts.push(`${cleanSec} ${secSize}: ${bits.join(", ")}`);
@@ -10370,29 +10424,33 @@ function buildSilhouetteContext(sizingResult, sizeGuide, selectedSizeOverride, u
10370
10424
  if (labelParts.length) out.recommendedSize = labelParts.join(", ");
10371
10425
  if (measurementParts.length) out.recommendedSizeMeasurements = measurementParts.join(" | ");
10372
10426
  }
10373
- const seen = /* @__PURE__ */ new Set();
10427
+ const seenUserMeasurements = /* @__PURE__ */ new Set();
10428
+ const seenSelectedMeasurements = /* @__PURE__ */ new Set();
10374
10429
  const userLines = [];
10375
- const push = (md2) => {
10430
+ const selectedMeasurementLines = [];
10431
+ const push = (md2, sectionName) => {
10376
10432
  if (!md2) return;
10377
10433
  for (const m2 of md2) {
10378
- const k2 = m2.measurement.toLowerCase();
10379
- if (seen.has(k2)) continue;
10380
- seen.add(k2);
10381
- if (m2.userValue) userLines.push(`${m2.measurement} ${formatUserMeasurementValue(m2.measurement, m2.userValue)}`);
10434
+ const normalizedMeasurement = m2.measurement.toLowerCase().replace(/\s*\(.*?\)\s*/g, "").trim();
10435
+ if (SKIP_AREAS_FOR_FIT.has(normalizedMeasurement)) continue;
10436
+ const selectedKey = sectionName ? `${sectionName.toLowerCase()}:${normalizedMeasurement}` : normalizedMeasurement;
10437
+ if (m2.userValue && !seenUserMeasurements.has(normalizedMeasurement)) {
10438
+ seenUserMeasurements.add(normalizedMeasurement);
10439
+ userLines.push(`${m2.measurement} ${normalizeMeasurementValueText(m2.measurement, m2.userValue, promptUnit)}`);
10440
+ }
10441
+ if (m2.chartRange && !seenSelectedMeasurements.has(selectedKey)) {
10442
+ seenSelectedMeasurements.add(selectedKey);
10443
+ const prefix = sectionName ? `${sectionName} ${m2.measurement}` : m2.measurement;
10444
+ selectedMeasurementLines.push(`${prefix} ${normalizeRangeText(m2.chartRange, promptUnit)}`);
10445
+ }
10382
10446
  }
10383
10447
  };
10384
10448
  push(sizingResult?.matchDetails);
10385
10449
  if (sizingResult?.sections) {
10386
- for (const sec of Object.values(sizingResult.sections)) push(sec.matchDetails);
10450
+ for (const [secName, sec] of Object.entries(sizingResult.sections)) push(sec.matchDetails, secName);
10387
10451
  }
10388
10452
  if (userLines.length) out.userMeasurementsText = userLines.join(", ");
10389
- if (sizeGuide?.headers?.length && sizeGuide.rows?.length) {
10390
- const hdrs = sizeGuide.headers;
10391
- const rowStrings = sizeGuide.rows.slice(0, 20).map(
10392
- (row) => hdrs.map((h, i) => `${h} ${row[i] ?? "—"}`).join(", ")
10393
- );
10394
- out.sizeChartSummary = rowStrings.join(" | ");
10395
- }
10453
+ if (selectedMeasurementLines.length) out.recommendedSizeMeasurements = selectedMeasurementLines.join(", ");
10396
10454
  return Object.keys(out).length ? out : void 0;
10397
10455
  }
10398
10456
  const FALLBACK_FIELDS_FEMALE = [
@@ -11627,7 +11685,7 @@ function productFitTypeToMeasurementType(fitType) {
11627
11685
  }
11628
11686
  return "body";
11629
11687
  }
11630
- const STYLES$1 = `
11688
+ const STYLES = `
11631
11689
  /* Variable defaults must live on BOTH the root (for the trigger button)
11632
11690
  and the overlay (which is React-portaled to <body> and therefore not
11633
11691
  a descendant of .ps-tryon-root, so the cascade is broken). Without
@@ -11694,6 +11752,7 @@ const STYLES$1 = `
11694
11752
  position: fixed; inset: 0; background: var(--ps-modal-overlay, rgba(0,0,0,0.6));
11695
11753
  display: flex; align-items: center; justify-content: center;
11696
11754
  z-index: 2147483647;
11755
+ pointer-events: auto;
11697
11756
  isolation: isolate;
11698
11757
  contain: layout style;
11699
11758
  padding: 0.83vw;
@@ -12183,11 +12242,20 @@ const STYLES$1 = `
12183
12242
  min-height: 0;
12184
12243
  }
12185
12244
  .ps-tryon-v2-result-panel .ps-tryon-photo-strip {
12186
- flex: 0 1 auto;
12245
+ flex: 0 0 auto;
12187
12246
  min-height: 0;
12247
+ margin-top: clamp(10px, 0.75vw, 14px);
12248
+ margin-bottom: clamp(8px, 0.7vw, 12px);
12188
12249
  }
12189
12250
  .ps-tryon-v2-result-panel .ps-tryon-sr-card-v2 {
12190
- min-height: 10.5vw;
12251
+ min-height: clamp(140px, 10.5vw, 165px);
12252
+ }
12253
+ .ps-tryon-v2-result-panel .ps-tryon-sr-card-v2.ps-full {
12254
+ min-height: clamp(140px, 10.5vw, 165px);
12255
+ }
12256
+ .ps-tryon-v2-result-panel .ps-tryon-photo-strip-cell {
12257
+ aspect-ratio: 1 / 1;
12258
+ height: auto;
12191
12259
  }
12192
12260
  .ps-tryon-v2-result-actions {
12193
12261
  display: flex;
@@ -12682,9 +12750,12 @@ const STYLES$1 = `
12682
12750
  .ps-tryon-photo-strip-row {
12683
12751
  display: grid;
12684
12752
  grid-template-columns: repeat(3, 1fr);
12685
- gap: 0.55vw;
12753
+ gap: 0.45vw;
12686
12754
  animation: ps-tryon-photo-strip-fade 0.5s ease;
12687
12755
  }
12756
+ .ps-tryon-photo-strip-row.ps-count-1 .ps-tryon-photo-strip-cell {
12757
+ grid-column: 2;
12758
+ }
12688
12759
  .ps-tryon-photo-strip-cell {
12689
12760
  position: relative;
12690
12761
  display: block;
@@ -12707,7 +12778,7 @@ const STYLES$1 = `
12707
12778
  object-position: center center;
12708
12779
  padding: 0;
12709
12780
  box-sizing: border-box;
12710
- transform: scale(0.94);
12781
+ transform: none;
12711
12782
  transform-origin: center center;
12712
12783
  user-select: none;
12713
12784
  pointer-events: none;
@@ -12748,7 +12819,7 @@ const STYLES$1 = `
12748
12819
  }
12749
12820
  .ps-tryon-photo-strip-cell > img {
12750
12821
  object-position: center center;
12751
- transform: scale(0.94);
12822
+ transform: none;
12752
12823
  }
12753
12824
  }
12754
12825
 
@@ -22269,7 +22340,7 @@ function ProductPhotoCarouselCard({
22269
22340
  const slide = entries.slice(start, start + PER_SLIDE);
22270
22341
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-tryon-photo-strip", role: "group", "aria-label": t2("Product photos"), children: [
22271
22342
  isCompleteLook && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-photo-strip-head", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ps-tryon-photo-strip-badge", children: t2("Complete the look") }) }),
22272
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-photo-strip-row", style: { gridTemplateColumns: `repeat(${Math.max(1, Math.min(PER_SLIDE, slide.length))}, 1fr)` }, children: slide.map((item, i) => {
22343
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `ps-tryon-photo-strip-row ps-count-${slide.length}`, children: slide.map((item, i) => {
22273
22344
  const content = /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: item.image, alt: item.title || productTitle || "", draggable: false });
22274
22345
  return item.href ? /* @__PURE__ */ jsxRuntimeExports.jsx(
22275
22346
  "a",
@@ -32102,7 +32173,7 @@ if (typeof document !== "undefined") {
32102
32173
  if (!document.getElementById(id2)) {
32103
32174
  const el2 = document.createElement("style");
32104
32175
  el2.id = id2;
32105
- el2.textContent = STYLES$1;
32176
+ el2.textContent = STYLES;
32106
32177
  document.head.appendChild(el2);
32107
32178
  }
32108
32179
  }
@@ -32154,6 +32225,27 @@ function sanitizeHistoryEntry(entry) {
32154
32225
  hasResult: entry.hasResult || !!entry.resultImageUrl || void 0
32155
32226
  };
32156
32227
  }
32228
+ function comparableProductId(value) {
32229
+ return String(value || "").trim().replace(/^gid:\/\/shopify\/Product\//i, "").replace(/^Product\//i, "");
32230
+ }
32231
+ function historyEntryMatchesProduct(entry, productId, productTitle) {
32232
+ const currentId = comparableProductId(productId);
32233
+ const entryId = comparableProductId(entry.productId);
32234
+ if (currentId && entryId) return currentId === entryId;
32235
+ const currentTitle = String(productTitle || "").trim().toLowerCase();
32236
+ const entryTitle = String(entry.productTitle || "").trim().toLowerCase();
32237
+ return !!currentTitle && !!entryTitle && currentTitle === entryTitle;
32238
+ }
32239
+ function hasCompletedTryOnResult(entry) {
32240
+ return !!(entry.resultImageUrl || entry.hasResult);
32241
+ }
32242
+ function dispatchTryOnCompleteEvent(detail) {
32243
+ if (typeof window === "undefined") return;
32244
+ try {
32245
+ window.dispatchEvent(new CustomEvent("primestyle:tryon-complete", { detail }));
32246
+ } catch {
32247
+ }
32248
+ }
32157
32249
  function computeMatchScore(recData) {
32158
32250
  if (!recData) return null;
32159
32251
  const all = [];
@@ -32209,6 +32301,7 @@ function PrimeStyleTryonInner({
32209
32301
  productDescription,
32210
32302
  productMaterial,
32211
32303
  buttonText,
32304
+ limitOneColorTryOnPerProduct = false,
32212
32305
  apiUrl,
32213
32306
  showPoweredBy = true,
32214
32307
  showIcon = true,
@@ -33201,6 +33294,14 @@ function PrimeStyleTryonInner({
33201
33294
  cleanupJob();
33202
33295
  setTryOnProcessing(false);
33203
33296
  setTryOnStartedAt(null);
33297
+ dispatchTryOnCompleteEvent({
33298
+ jobId: update.galleryId,
33299
+ historyEntryId: currentHistoryEntryIdRef.current,
33300
+ productId: effectiveProductId,
33301
+ productTitle,
33302
+ recommendedSize: sizingResultRef.current?.recommendedSize,
33303
+ resultImageUrl: update.imageUrl
33304
+ });
33204
33305
  onComplete?.({ jobId: update.galleryId, imageUrl: update.imageUrl });
33205
33306
  }
33206
33307
  } else if (update.status === "failed") {
@@ -34404,6 +34505,21 @@ function PrimeStyleTryonInner({
34404
34505
  setDrawer(null);
34405
34506
  setView("size-result");
34406
34507
  }, [cleanupJob, productId, productTitle, effectiveProductImages, effectiveProductCarouselItems]);
34508
+ const savedOneColorFitEntry = reactExports.useMemo(() => {
34509
+ if (!limitOneColorTryOnPerProduct) return null;
34510
+ return history.find(
34511
+ (entry) => hasCompletedTryOnResult(entry) && !!entry.recommendedSize && historyEntryMatchesProduct(entry, effectiveProductId, productTitle)
34512
+ ) || null;
34513
+ }, [limitOneColorTryOnPerProduct, history, effectiveProductId, productTitle]);
34514
+ const storefrontButtonText = savedOneColorFitEntry?.recommendedSize ? `${t2("Your size is")} ${savedOneColorFitEntry.recommendedSize}` : resolvedButtonText;
34515
+ const handleStorefrontButtonClick = reactExports.useCallback(() => {
34516
+ if (savedOneColorFitEntry) {
34517
+ onOpen?.();
34518
+ restoreHistory(savedOneColorFitEntry);
34519
+ return;
34520
+ }
34521
+ handleOpen();
34522
+ }, [handleOpen, onOpen, restoreHistory, savedOneColorFitEntry]);
34407
34523
  const updateField = reactExports.useCallback((key, val) => {
34408
34524
  formRef.current[key] = val;
34409
34525
  }, []);
@@ -34873,15 +34989,12 @@ function PrimeStyleTryonInner({
34873
34989
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
34874
34990
  "button",
34875
34991
  {
34876
- onClick: (e) => {
34877
- console.log("[ps-sdk] button click event fired", e);
34878
- handleOpen();
34879
- },
34992
+ onClick: handleStorefrontButtonClick,
34880
34993
  className: cx(cx("ps-tryon-btn", isTextTrigger ? "ps-tryon-btn--text" : void 0), cn.button),
34881
34994
  type: "button",
34882
34995
  children: [
34883
34996
  !isTextTrigger && showIcon !== false && (buttonIcon || /* @__PURE__ */ jsxRuntimeExports.jsx(CameraIcon$1, {})),
34884
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: resolvedButtonText })
34997
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: storefrontButtonText })
34885
34998
  ]
34886
34999
  }
34887
35000
  ),
@@ -35159,7 +35272,7 @@ class PrimeStyleTryonErrorBoundary extends reactExports.Component {
35159
35272
  function PrimeStyleTryon(props) {
35160
35273
  return /* @__PURE__ */ jsxRuntimeExports.jsx(PrimeStyleTryonErrorBoundary, { ...props });
35161
35274
  }
35162
- const STYLES = `
35275
+ const SIZE_GUIDE_STYLES = `
35163
35276
  .ps-sg-btn {
35164
35277
  display: inline-flex;
35165
35278
  align-items: center;
@@ -35322,7 +35435,7 @@ function injectStyles() {
35322
35435
  if (stylesInjected) return;
35323
35436
  const tag = document.createElement("style");
35324
35437
  tag.id = "ps-sg-styles";
35325
- tag.textContent = STYLES;
35438
+ tag.textContent = SIZE_GUIDE_STYLES;
35326
35439
  document.head.appendChild(tag);
35327
35440
  stylesInjected = true;
35328
35441
  }
@@ -35514,10 +35627,112 @@ function installProductViewTracking() {
35514
35627
  roots.forEach((r2) => maybeFireProductView(r2));
35515
35628
  }
35516
35629
  const TAG = "[primestyle-tryon]";
35517
- console.log(`${TAG} bundle loaded — version 5.7.x storefront entry`);
35630
+ const SHADOW_STYLE_ATTR = "data-primestyle-sdk";
35631
+ const DOCUMENT_STYLE_ATTR = "data-primestyle-sdk-document";
35632
+ const SHADOW_MOUNT_ATTR = "data-primestyle-mount";
35633
+ const PORTAL_HOST_ATTR = "data-primestyle-portal-host";
35634
+ const PORTAL_MOUNT_ATTR = "data-primestyle-portal-mount";
35635
+ const SDK_CSS = `${STYLES}
35636
+
35637
+ ${SIZE_GUIDE_STYLES}`;
35638
+ console.log(`${TAG} storefront bundle loaded`);
35518
35639
  installCartHook();
35519
35640
  installProductViewTracking();
35520
35641
  const MOUNTED = /* @__PURE__ */ new WeakMap();
35642
+ const MOUNT_SIGNATURES = /* @__PURE__ */ new WeakMap();
35643
+ const MOUNTING = /* @__PURE__ */ new WeakSet();
35644
+ function ensureDocumentStyles() {
35645
+ if (typeof document === "undefined" || !document.head) return;
35646
+ let styleTag = document.head.querySelector(`style[${DOCUMENT_STYLE_ATTR}]`);
35647
+ if (!styleTag) {
35648
+ styleTag = document.createElement("style");
35649
+ styleTag.setAttribute(DOCUMENT_STYLE_ATTR, "true");
35650
+ document.head.appendChild(styleTag);
35651
+ }
35652
+ if (styleTag.textContent !== SDK_CSS) {
35653
+ styleTag.textContent = SDK_CSS;
35654
+ }
35655
+ }
35656
+ function getPortalHost() {
35657
+ if (typeof document === "undefined" || !document.body) return void 0;
35658
+ let portalHost = document.querySelector(`[${PORTAL_HOST_ATTR}]`);
35659
+ if (!portalHost) {
35660
+ portalHost = document.createElement("div");
35661
+ portalHost.setAttribute(PORTAL_HOST_ATTR, "true");
35662
+ document.body.appendChild(portalHost);
35663
+ }
35664
+ portalHost.style.setProperty("display", "block", "important");
35665
+ portalHost.style.setProperty("position", "fixed", "important");
35666
+ portalHost.style.setProperty("inset", "0", "important");
35667
+ portalHost.style.setProperty("width", "100vw", "important");
35668
+ portalHost.style.setProperty("height", "100vh", "important");
35669
+ portalHost.style.setProperty("z-index", "2147483647", "important");
35670
+ portalHost.style.setProperty("visibility", "visible", "important");
35671
+ portalHost.style.setProperty("opacity", "1", "important");
35672
+ portalHost.style.setProperty("pointer-events", "none", "important");
35673
+ portalHost.style.setProperty("transform", "none", "important");
35674
+ portalHost.style.setProperty("margin", "0", "important");
35675
+ portalHost.style.setProperty("padding", "0", "important");
35676
+ if (typeof portalHost.attachShadow === "function") {
35677
+ try {
35678
+ const shadow = portalHost.shadowRoot || portalHost.attachShadow({ mode: "open" });
35679
+ let styleTag = shadow.querySelector(`style[${SHADOW_STYLE_ATTR}]`);
35680
+ if (!styleTag) {
35681
+ styleTag = document.createElement("style");
35682
+ styleTag.setAttribute(SHADOW_STYLE_ATTR, "true");
35683
+ shadow.appendChild(styleTag);
35684
+ }
35685
+ if (styleTag.textContent !== SDK_CSS) {
35686
+ styleTag.textContent = SDK_CSS;
35687
+ }
35688
+ let portalMount = shadow.querySelector(`[${PORTAL_MOUNT_ATTR}]`);
35689
+ if (!portalMount) {
35690
+ portalMount = document.createElement("div");
35691
+ portalMount.setAttribute(PORTAL_MOUNT_ATTR, "true");
35692
+ portalMount.className = "ps-tryon-portal-mount";
35693
+ shadow.appendChild(portalMount);
35694
+ }
35695
+ portalMount.style.setProperty("display", "block", "important");
35696
+ portalMount.style.setProperty("width", "100%", "important");
35697
+ portalMount.style.setProperty("height", "100%", "important");
35698
+ portalMount.style.setProperty("pointer-events", "none", "important");
35699
+ return portalMount;
35700
+ } catch (err) {
35701
+ console.warn(`${TAG} portal shadow DOM mount failed; falling back to page DOM`, err);
35702
+ }
35703
+ }
35704
+ ensureDocumentStyles();
35705
+ return portalHost;
35706
+ }
35707
+ function createShadowMountTarget(el2) {
35708
+ if (!(el2 instanceof HTMLElement) || typeof el2.attachShadow !== "function") {
35709
+ return { mountTarget: el2 };
35710
+ }
35711
+ try {
35712
+ const shadow = el2.shadowRoot || el2.attachShadow({ mode: "open" });
35713
+ let styleTag = shadow.querySelector(`style[${SHADOW_STYLE_ATTR}]`);
35714
+ if (!styleTag) {
35715
+ styleTag = document.createElement("style");
35716
+ styleTag.setAttribute(SHADOW_STYLE_ATTR, "true");
35717
+ shadow.appendChild(styleTag);
35718
+ }
35719
+ if (styleTag.textContent !== SDK_CSS) {
35720
+ styleTag.textContent = SDK_CSS;
35721
+ }
35722
+ let mountTarget = shadow.querySelector(`[${SHADOW_MOUNT_ATTR}]`);
35723
+ if (!mountTarget) {
35724
+ mountTarget = document.createElement("div");
35725
+ mountTarget.setAttribute(SHADOW_MOUNT_ATTR, "true");
35726
+ mountTarget.className = "ps-tryon-shadow-mount";
35727
+ shadow.appendChild(mountTarget);
35728
+ }
35729
+ el2.setAttribute("data-primestyle-shadow", "true");
35730
+ return { mountTarget, portalContainer: getPortalHost() ?? document.body };
35731
+ } catch (err) {
35732
+ console.warn(`${TAG} shadow DOM mount failed; falling back to page DOM`, err);
35733
+ return { mountTarget: el2 };
35734
+ }
35735
+ }
35521
35736
  function readDataAttrs(el2) {
35522
35737
  const out = {};
35523
35738
  for (const attr of Array.from(el2.attributes)) {
@@ -35536,6 +35751,13 @@ function parseJsonAttr(raw, fallback) {
35536
35751
  return fallback;
35537
35752
  }
35538
35753
  }
35754
+ function buildMountSignature(data) {
35755
+ const relevant = {};
35756
+ for (const key of Object.keys(data).sort()) {
35757
+ relevant[key] = data[key];
35758
+ }
35759
+ return JSON.stringify(relevant);
35760
+ }
35539
35761
  function resolveProductUrl(raw) {
35540
35762
  const source = raw || (typeof window !== "undefined" ? window.location.href : "");
35541
35763
  if (!source) return void 0;
@@ -35556,13 +35778,18 @@ function buildPropsFromDataAttrs(data) {
35556
35778
  productTitle: data.productTitle,
35557
35779
  productUrl: resolveProductUrl(data.productUrl),
35558
35780
  productImage: data.productImage,
35559
- productCarouselItems: parseJsonAttr(data.productCarouselItems, void 0),
35781
+ productImages: parseJsonAttr(data.productImages, void 0),
35782
+ productCarouselItems: parseJsonAttr(
35783
+ data.productCarouselItems,
35784
+ void 0
35785
+ ),
35560
35786
  productDescription: data.productDescription,
35561
35787
  productGender: data.productGender,
35562
35788
  productType: data.productType,
35563
35789
  productVendor: data.productVendor,
35564
35790
  productTags: data.productTags ? data.productTags.split(",").map((t2) => t2.trim()).filter(Boolean) : void 0,
35565
35791
  buttonText: data.buttonText || "Find Your Size",
35792
+ limitOneColorTryOnPerProduct: data.limitOneColorTryOnPerProduct === "true",
35566
35793
  locale: data.locale,
35567
35794
  sizeGuideData: parseJsonAttr(data.cachedSizeguide, void 0),
35568
35795
  sizingCountry: data.sizingCountry,
@@ -35599,22 +35826,39 @@ async function fetchSizeGuideForProduct(proxyUrl, productId) {
35599
35826
  }
35600
35827
  }
35601
35828
  async function mount(el2) {
35602
- if (MOUNTED.has(el2)) {
35603
- console.log(`${TAG} already mounted on`, el2);
35829
+ const data = readDataAttrs(el2);
35830
+ const signature = buildMountSignature(data);
35831
+ const existingRoot = MOUNTED.get(el2);
35832
+ if (existingRoot) {
35833
+ if (MOUNT_SIGNATURES.get(el2) === signature) {
35834
+ console.log(`${TAG} already mounted on`, el2);
35835
+ return;
35836
+ }
35837
+ console.log(`${TAG} data changed; remounting on`, el2);
35838
+ try {
35839
+ existingRoot.unmount();
35840
+ } catch (err) {
35841
+ console.warn(`${TAG} previous React root failed to unmount`, err);
35842
+ }
35843
+ MOUNTED.delete(el2);
35844
+ MOUNT_SIGNATURES.delete(el2);
35845
+ }
35846
+ if (MOUNTING.has(el2)) {
35847
+ console.log(`${TAG} mount already in progress on`, el2);
35604
35848
  return;
35605
35849
  }
35850
+ MOUNTING.add(el2);
35606
35851
  console.log(`${TAG} mounting on element`, el2);
35607
- const data = readDataAttrs(el2);
35608
35852
  console.log(`${TAG} read data attributes`, data);
35609
35853
  const props = buildPropsFromDataAttrs(data);
35610
- const hadCachedSizeguide = !!data.cachedSizeguide;
35854
+ const shouldRenderStandaloneSizeGuide = !!data.cachedSizeguide && data.disableSizeGuideButton !== "true";
35611
35855
  if (!props.sizeGuideData && props.productId && props.apiUrl) {
35612
35856
  const fetched = await fetchSizeGuideForProduct(props.apiUrl, props.productId);
35613
35857
  if (fetched) {
35614
35858
  props.sizeGuideData = fetched;
35615
35859
  }
35616
35860
  }
35617
- if (hadCachedSizeguide && props.sizeGuideData) {
35861
+ if (shouldRenderStandaloneSizeGuide && props.sizeGuideData) {
35618
35862
  try {
35619
35863
  const buttonStyles = props.buttonStyles;
35620
35864
  createSizeGuideButton(el2, props.sizeGuideData, {
@@ -35626,13 +35870,22 @@ async function mount(el2) {
35626
35870
  }
35627
35871
  }
35628
35872
  try {
35629
- const root = createRoot(el2);
35630
- root.render(reactExports.createElement(PrimeStyleTryon, props));
35873
+ const { mountTarget, portalContainer } = createShadowMountTarget(el2);
35874
+ const root = createRoot(mountTarget);
35875
+ root.render(
35876
+ reactExports.createElement(PrimeStyleTryon, {
35877
+ ...props,
35878
+ portalContainer
35879
+ })
35880
+ );
35631
35881
  MOUNTED.set(el2, root);
35882
+ MOUNT_SIGNATURES.set(el2, signature);
35632
35883
  console.log(`${TAG} ✓ mounted React component`);
35633
35884
  maybeFireProductView(el2);
35634
35885
  } catch (err) {
35635
35886
  console.error(`${TAG} ✗ React mount failed`, err);
35887
+ } finally {
35888
+ MOUNTING.delete(el2);
35636
35889
  }
35637
35890
  }
35638
35891
  function mountAll() {
@@ -29,6 +29,7 @@ interface ButtonOptions {
29
29
  label?: string;
30
30
  accentColor?: string;
31
31
  }
32
+ export declare const SIZE_GUIDE_STYLES = "\n .ps-sg-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 8px 14px;\n margin: 0 0 10px 0;\n background: transparent;\n border: 1px solid rgba(0, 0, 0, 0.12);\n border-radius: 8px;\n color: var(--ps-sg-accent, #2154EF);\n font-size: 13px;\n font-weight: 600;\n font-family: inherit;\n cursor: pointer;\n transition: background 0.15s, border-color 0.15s;\n }\n .ps-sg-btn:hover {\n background: rgba(33, 84, 239, 0.04);\n border-color: var(--ps-sg-accent, #2154EF);\n }\n .ps-sg-btn svg { width: 14px; height: 14px; }\n .ps-sg-overlay {\n position: fixed; inset: 0;\n background: rgba(15, 23, 42, 0.55);\n display: flex; align-items: center; justify-content: center;\n z-index: 99999;\n padding: 16px;\n opacity: 0;\n animation: ps-sg-fadein 0.18s ease forwards;\n }\n @keyframes ps-sg-fadein { to { opacity: 1; } }\n .ps-sg-modal {\n background: #FFFFFF;\n border-radius: 14px;\n width: 100%;\n max-width: 720px;\n max-height: 88vh;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n box-shadow: 0 24px 60px rgba(15, 23, 42, 0.25);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n color: #1f2937;\n }\n .ps-sg-header {\n display: flex; align-items: center; justify-content: space-between;\n padding: 18px 22px 14px;\n border-bottom: 1px solid rgba(15, 23, 42, 0.08);\n }\n .ps-sg-title {\n margin: 0;\n font-size: 17px;\n font-weight: 700;\n }\n .ps-sg-toggle {\n display: inline-flex;\n background: rgba(15, 23, 42, 0.06);\n border-radius: 8px;\n padding: 3px;\n margin-right: 12px;\n }\n .ps-sg-toggle button {\n padding: 5px 14px;\n background: transparent;\n border: none;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n color: rgba(15, 23, 42, 0.55);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n font-family: inherit;\n }\n .ps-sg-toggle button.ps-sg-active {\n background: #FFFFFF;\n color: #1f2937;\n box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);\n }\n .ps-sg-close {\n background: transparent;\n border: none;\n color: rgba(15, 23, 42, 0.55);\n cursor: pointer;\n padding: 4px;\n border-radius: 6px;\n font-family: inherit;\n }\n .ps-sg-close:hover { background: rgba(15, 23, 42, 0.06); }\n .ps-sg-close svg { width: 18px; height: 18px; }\n .ps-sg-body {\n padding: 18px 22px 22px;\n overflow-y: auto;\n flex: 1;\n }\n .ps-sg-section { margin-bottom: 22px; }\n .ps-sg-section:last-child { margin-bottom: 0; }\n .ps-sg-section-title {\n margin: 0 0 6px;\n font-size: 14px;\n font-weight: 700;\n color: #0f172a;\n }\n .ps-sg-section-desc {\n margin: 0 0 10px;\n font-size: 12px;\n color: rgba(15, 23, 42, 0.6);\n }\n .ps-sg-table-wrap {\n overflow-x: auto;\n border: 1px solid rgba(15, 23, 42, 0.08);\n border-radius: 10px;\n }\n .ps-sg-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n .ps-sg-table thead th {\n background: rgba(15, 23, 42, 0.04);\n padding: 10px 12px;\n text-align: left;\n font-weight: 700;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: rgba(15, 23, 42, 0.6);\n border-bottom: 1px solid rgba(15, 23, 42, 0.08);\n }\n .ps-sg-table tbody td {\n padding: 10px 12px;\n border-bottom: 1px solid rgba(15, 23, 42, 0.05);\n }\n .ps-sg-table tbody tr:last-child td { border-bottom: none; }\n .ps-sg-htm {\n margin-top: 18px;\n padding: 14px 16px;\n background: rgba(33, 84, 239, 0.05);\n border-radius: 10px;\n border: 1px solid rgba(33, 84, 239, 0.12);\n }\n .ps-sg-htm-title {\n font-size: 12px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--ps-sg-accent, #2154EF);\n margin-bottom: 8px;\n }\n .ps-sg-htm ul {\n margin: 0;\n padding-left: 18px;\n font-size: 12px;\n line-height: 1.6;\n color: #1f2937;\n }\n";
32
33
  /**
33
34
  * Create a "Size Guide" button and insert it as a sibling above the
34
35
  * given parent element. Returns a teardown function that removes the
package/dist/types.d.ts CHANGED
@@ -260,9 +260,9 @@ export interface FitAreaInfo {
260
260
  section?: string;
261
261
  /** Fit classification */
262
262
  fit: "good" | "tight" | "loose" | "a-bit-tight" | "a-bit-loose" | "too-tight" | "too-loose";
263
- /** User's measurement value in cm */
263
+ /** User's measurement value in the same unit as garmentRange */
264
264
  userValue?: number;
265
- /** Garment's size chart range for this area (e.g. "96-100") */
265
+ /** Garment's selected-size range for this area (e.g. "37-38 in") */
266
266
  garmentRange?: string;
267
267
  /** Normalised body coordinate — vertical position (0 = top, 1 = bottom) */
268
268
  y?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "5.10.178",
3
+ "version": "5.10.179",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",