@primestyleai/tryon 5.10.103 → 5.10.105

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.
@@ -139,10 +139,21 @@ export type ViewState = "idle" | "welcome" | "body-profile" | "estimation-review
139
139
  export type DrawerType = "profiles" | "history" | null;
140
140
  export interface PrimeStyleTryonProps {
141
141
  productImage: string;
142
- /** Optional gallery of product photos for the active color/variant. Shown
143
- * as an auto-cycling carousel on the single-garment SizeResultView while
144
- * the try-on image is being generated, to keep the user entertained. */
142
+ /** Optional gallery of product photos for the active color/variant.
143
+ *
144
+ * - Shown as an auto-cycling carousel on the single-garment
145
+ * SizeResultView while the try-on image is being generated.
146
+ * - At try-on time the SDK runs MediaPipe BlazePose on every entry in
147
+ * parallel and forwards the highest-scoring "model wearing the
148
+ * garment" shot to Gemini as image 2. Flat-lays / close-up details
149
+ * score ~0 and are skipped. The first image still shows in the UI
150
+ * unchanged. */
145
151
  productImages?: string[];
152
+ /** Optional explicit override — if you already know which image in the
153
+ * gallery is the model-wearing-the-garment shot, pass it here and the
154
+ * SDK skips the MediaPipe-based auto-pick. Falls through to auto-pick
155
+ * (or to `productImage`) when omitted. */
156
+ garmentReferenceImage?: string;
146
157
  productTitle?: string;
147
158
  /** Stable product identifier — used to cache size recommendations per (profile, product) */
148
159
  productId?: string;
@@ -0,0 +1 @@
1
+ export declare function pickBestGarmentImage(images: string[] | null | undefined): Promise<string | null>;
@@ -21,6 +21,13 @@ export declare function setLastCompletedProduct(productId: string | null, produc
21
21
  * the size-result view. The cart-hook then fires a SIZE_RECOMMENDATION_ACCEPTED
22
22
  * event when this product gets added to cart — a clearer "user committed"
23
23
  * signal than "user clicked a pill once".
24
+ *
25
+ * Side-effect: proactively stamp the current cart with `attributes[ps_session]`
26
+ * via `/cart/update.js`. This is the primary attribution path now — works
27
+ * regardless of how the buyer eventually checks out (Ajax cart, Buy It Now,
28
+ * Shop Pay), because the attribute is already on the cart object before
29
+ * the buyer adds anything. The legacy fetch/form-submit interception below
30
+ * stays as a defense-in-depth fallback for older themes.
24
31
  */
25
32
  export declare function setLastSizeSelection(input: {
26
33
  productId: string | null;
@@ -1,4 +1,46 @@
1
1
  "use client";
2
+ const TAG$3 = "[primestyle-shadow]";
3
+ const collected = [];
4
+ if (typeof document !== "undefined" && document.head) {
5
+ let shouldCapture = function(node) {
6
+ return node instanceof HTMLStyleElement;
7
+ };
8
+ const origAppend = document.head.appendChild.bind(document.head);
9
+ const origInsertBefore = document.head.insertBefore.bind(document.head);
10
+ document.head.appendChild = function(node) {
11
+ if (shouldCapture(node)) {
12
+ collected.push(node);
13
+ console.log(`${TAG$3} captured style tag (${node.textContent?.length ?? 0} chars)`);
14
+ }
15
+ return origAppend(node);
16
+ };
17
+ document.head.insertBefore = function(node, ref) {
18
+ if (shouldCapture(node)) {
19
+ collected.push(node);
20
+ console.log(`${TAG$3} captured style tag via insertBefore (${node.textContent?.length ?? 0} chars)`);
21
+ }
22
+ return origInsertBefore(node, ref);
23
+ };
24
+ }
25
+ function getCollectedCss() {
26
+ const parts = [];
27
+ for (const el2 of collected) {
28
+ const text = el2.textContent;
29
+ if (text) parts.push(text);
30
+ }
31
+ return parts.join("\n\n");
32
+ }
33
+ function injectStylesIntoShadow(shadow) {
34
+ const css = getCollectedCss();
35
+ if (!css) {
36
+ console.warn(`${TAG$3} no SDK styles collected — widget may render unstyled`);
37
+ return;
38
+ }
39
+ const style = document.createElement("style");
40
+ style.setAttribute("data-primestyle", "sdk-styles");
41
+ style.textContent = css;
42
+ shadow.appendChild(style);
43
+ }
2
44
  var react = { exports: {} };
3
45
  var react_production_min = {};
4
46
  /**
@@ -10588,6 +10630,45 @@ function getUnitLabel(unit) {
10588
10630
  if (unit === "mm") return "mm";
10589
10631
  return "";
10590
10632
  }
10633
+ const cache = /* @__PURE__ */ new Map();
10634
+ function scoreLandmarks(lm) {
10635
+ if (!lm) return 0;
10636
+ let joints = 0;
10637
+ for (const [k2, v2] of Object.entries(lm)) {
10638
+ if (k2 === "imageWidth" || k2 === "imageHeight") continue;
10639
+ if (v2 && typeof v2 === "object" && typeof v2.x === "number") joints++;
10640
+ }
10641
+ let score = joints * 10;
10642
+ if (lm.nose) score += 50;
10643
+ if (lm.leftAnkle && lm.rightAnkle) score += 5;
10644
+ return score;
10645
+ }
10646
+ async function scoreImage(url) {
10647
+ try {
10648
+ const lm = await detectBodyLandmarks(url);
10649
+ return scoreLandmarks(lm);
10650
+ } catch {
10651
+ return 0;
10652
+ }
10653
+ }
10654
+ async function pickBestGarmentImage(images) {
10655
+ if (!images || !images.length) return null;
10656
+ if (images.length === 1) return images[0];
10657
+ const cacheKey = images.join("|");
10658
+ const cached = cache.get(cacheKey);
10659
+ if (cached) return cached;
10660
+ const t0 = Date.now();
10661
+ const scored = await Promise.all(images.map(async (url) => ({
10662
+ url,
10663
+ score: await scoreImage(url)
10664
+ })));
10665
+ scored.sort((a, b) => b.score - a.score);
10666
+ const best = (scored[0]?.score ?? 0) > 0 ? scored[0].url : images[0];
10667
+ cache.set(cacheKey, best);
10668
+ console.log(`[ps-sdk:garment-pick] ${Date.now() - t0}ms — chose ${images.indexOf(best)}/${images.length}`);
10669
+ for (const s of scored) console.log(`[ps-sdk:garment-pick] ${s.score.toString().padStart(4, " ")} ${s.url}`);
10670
+ return best;
10671
+ }
10591
10672
  function cx(base, override) {
10592
10673
  return override ? `${base} ${override}` : base;
10593
10674
  }
@@ -19379,14 +19460,6 @@ function ProductPhotoCarouselCard({
19379
19460
  slide.push(photos[(start + slide.length) % photos.length]);
19380
19461
  }
19381
19462
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ps-tryon-photo-strip", role: "group", "aria-label": t2("Product photos"), children: [
19382
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-photo-strip-head", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ps-tryon-photo-strip-badge", children: [
19383
- /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "11", height: "11", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.4", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
19384
- /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
19385
- /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "9", cy: "9", r: "2" }),
19386
- /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" })
19387
- ] }),
19388
- t2("Gallery")
19389
- ] }) }),
19390
19463
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-photo-strip-row", children: slide.map((src, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-photo-strip-cell", children: /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src, alt: productTitle || "", draggable: false }) }, `${groupIdx}-${i}`)) }, groupIdx),
19391
19464
  totalGroups > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ps-tryon-photo-strip-dots", "aria-hidden": "true", children: Array.from({ length: totalGroups }).map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `ps-tryon-photo-strip-dot${i === groupIdx ? " is-active" : ""}` }, i)) })
19392
19465
  ] });
@@ -20027,103 +20100,26 @@ function SectionDetailView({
20027
20100
  return details.map((m2) => {
20028
20101
  if (isFromLength.has(m2.measurement)) {
20029
20102
  const userNum2 = userMeasurements[m2.measurement.toLowerCase()] || pNumFn(m2.userValue);
20030
- const activeLength = selectedLength || effectiveRecLength || m2.chartRange;
20031
- if (!lengthEntry) {
20032
- return { area: m2.measurement + " (" + activeLength + ")", userNum: userNum2, chartLabel: activeLength, fit: "good", isLength: true };
20033
- }
20034
- const sec = lengthEntry.section;
20035
- const measLc = m2.measurement.toLowerCase();
20036
- const isHeight = measLc === "height";
20037
- const sizeCol = sec.headers.findIndex((h) => /size|length/i.test(h.trim()));
20038
- const userIsInches = unitLbl === "in";
20039
- let targetColIdx = -1;
20040
- let colIsCm = false;
20041
- if (isHeight) {
20042
- const cmColIdx = sec.headers.findIndex((h) => /cm|\(cm\)|height.*cm/i.test(h.toLowerCase()));
20043
- const genericColIdx = sec.headers.findIndex((h) => /height|altezza|estatura/i.test(h.toLowerCase()) && !/cm/i.test(h));
20044
- targetColIdx = cmColIdx >= 0 ? cmColIdx : genericColIdx;
20045
- colIsCm = targetColIdx === cmColIdx;
20046
- } else {
20047
- targetColIdx = sec.headers.findIndex((h) => {
20048
- const hLc = h.toLowerCase().replace(/\s*\(.*?\)\s*/g, "").trim();
20049
- if (!hLc) return false;
20050
- return hLc === measLc || hLc.includes(measLc) || measLc.includes(hLc);
20051
- });
20052
- colIsCm = targetColIdx >= 0 ? /cm/i.test(sec.headers[targetColIdx] || "") : false;
20053
- }
20054
- const sIdx = sizeCol >= 0 ? sizeCol : 0;
20055
- const hIdx = targetColIdx >= 0 ? targetColIdx : -1;
20056
- const activeLengthForLookup = (() => {
20057
- const al2 = (activeLength || "").toLowerCase().trim();
20058
- if (/big.*tall|tall/.test(al2)) return "Long";
20059
- if (/^big$/.test(al2)) return "Regular";
20060
- return activeLength;
20061
- })();
20062
- const alLc = (activeLengthForLookup || "").toLowerCase().trim();
20063
- const matchRow = sec.rows.find((r2) => cellValFn(r2, sIdx, sec.headers[sIdx]) === activeLength) || sec.rows.find((r2) => cellValFn(r2, sIdx, sec.headers[sIdx]).trim().toLowerCase() === alLc) || null;
20064
- let chartLabel2 = activeLength;
20065
- let fit2 = "good";
20066
- if (matchRow && hIdx >= 0) {
20067
- const rangeStr = cellValFn(matchRow, hIdx, sec.headers[hIdx]);
20068
- if (rangeStr) {
20069
- const { min: rMinRaw, max: rMaxRaw } = pRangeFn(rangeStr);
20070
- if (rMinRaw > 0 && rMaxRaw > 0) {
20071
- const userInColUnit = colIsCm && userIsInches ? +(userNum2 * 2.54).toFixed(1) : !colIsCm && !userIsInches ? +(userNum2 * 2.54).toFixed(1) : userNum2;
20072
- const range2 = rMaxRaw - rMinRaw;
20073
- const threshold2 = range2 > 0 ? range2 * 0.5 : rMinRaw * 0.05 || 3;
20074
- const tol = colIsCm ? 2.54 : 1;
20075
- if (userInColUnit > rMinRaw - tol && userInColUnit < rMaxRaw + tol) fit2 = "good";
20076
- else if (userInColUnit < rMinRaw) {
20077
- const diff = rMinRaw - userInColUnit;
20078
- fit2 = diff > threshold2 * 2 ? "too-long" : diff > threshold2 ? "long" : "a-bit-long";
20079
- } else {
20080
- const diff = userInColUnit - rMaxRaw;
20081
- fit2 = diff > threshold2 * 2 ? "too-short" : diff > threshold2 ? "short" : "a-bit-short";
20082
- }
20083
- const needsConvert = colIsCm && userIsInches || !colIsCm && !userIsInches;
20084
- const rMinUser = needsConvert ? colIsCm ? +(rMinRaw / 2.54).toFixed(1) : +(rMinRaw * 2.54).toFixed(1) : rMinRaw;
20085
- const rMaxUser = needsConvert ? colIsCm ? +(rMaxRaw / 2.54).toFixed(1) : +(rMaxRaw * 2.54).toFixed(1) : rMaxRaw;
20086
- chartLabel2 = rMinUser === rMaxUser ? `${rMinUser}` : `${rMinUser}-${rMaxUser}`;
20087
- } else {
20088
- chartLabel2 = rangeStr;
20089
- }
20090
- }
20091
- }
20092
- return { area: m2.measurement + " (" + activeLength + ")", userNum: userNum2, chartLabel: cleanNumFn(chartLabel2), fit: fit2, isLength: true };
20103
+ return {
20104
+ area: m2.measurement,
20105
+ userNum: userNum2,
20106
+ chartLabel: cleanNumFn(m2.chartRange),
20107
+ fit: m2.fit || "good",
20108
+ isLength: true
20109
+ };
20093
20110
  }
20094
20111
  const userNum = userMeasurements[m2.measurement.toLowerCase()] || pNumFn(m2.userValue);
20095
- let { min: rMin, max: rMax } = pRangeFn(m2.chartRange);
20096
- let chartLabel = m2.chartRange;
20097
- const alt = chartRangeFor(m2.measurement, displaySize);
20098
- if (alt) {
20099
- chartLabel = alt.range;
20100
- rMin = alt.min;
20101
- rMax = alt.max;
20102
- }
20103
- const range = rMax - rMin;
20104
- const threshold = range > 0 ? range * 0.5 : rMin * 0.05 || 3;
20105
20112
  const measLower = m2.measurement.toLowerCase();
20106
20113
  const isDirectional = /length|inseam|sleeve|hem|rise/.test(measLower);
20107
- let fit;
20108
- const perfectTol = chartUnit === "cm" ? 2.54 : chartUnit === "mm" ? 25.4 : 1;
20109
- const lowBound = rMin - perfectTol;
20110
- const highBound = rMax + perfectTol;
20111
- if (userNum > lowBound && userNum < highBound) {
20112
- fit = "good";
20113
- } else if (isDirectional) {
20114
- const diff = userNum > rMax ? userNum - rMax : rMin - userNum;
20115
- const bucket = diff > threshold * 2 ? "too-" : diff > threshold ? "" : "a-bit-";
20116
- fit = bucket + (userNum > rMax ? "short" : "long");
20117
- } else if (userNum < rMin) {
20118
- const diff = rMin - userNum;
20119
- fit = diff > threshold * 2 ? "too-loose" : diff > threshold ? "loose" : "a-bit-loose";
20120
- } else {
20121
- const diff = userNum - rMax;
20122
- fit = diff > threshold * 2 ? "too-tight" : diff > threshold ? "tight" : "a-bit-tight";
20123
- }
20124
- return { area: m2.measurement, userNum, chartLabel: cleanNumFn(chartLabel), fit, isLength: isDirectional };
20114
+ return {
20115
+ area: m2.measurement,
20116
+ userNum,
20117
+ chartLabel: cleanNumFn(m2.chartRange),
20118
+ fit: m2.fit || "good",
20119
+ isLength: isDirectional
20120
+ };
20125
20121
  });
20126
- }, [sectionResult, lengthEntry, userMeasurements, displaySize, recSize, chartRangeFor, selectedLength, recLength, renderRaw]);
20122
+ }, [sectionResult, lengthEntry, userMeasurements, renderRaw]);
20127
20123
  const goodCount = fitRows.filter(
20128
20124
  (r2) => r2.fit === "good" || r2.fit === "a-bit-tight" || r2.fit === "a-bit-loose"
20129
20125
  ).length;
@@ -27672,6 +27668,7 @@ function measurementTypeToVtoCategory(type) {
27672
27668
  function PrimeStyleTryonInner({
27673
27669
  productImage,
27674
27670
  productImages,
27671
+ garmentReferenceImage,
27675
27672
  productTitle = "Product",
27676
27673
  productId,
27677
27674
  productDescription,
@@ -28837,9 +28834,19 @@ function PrimeStyleTryonInner({
28837
28834
  fitInfo = buildFitInfo(effectiveMatchDetails, modelPoseRef.current, unit);
28838
28835
  }
28839
28836
  console.log("[ps-sdk:tryon] fitInfo built", { count: fitInfo?.length || 0, areas: fitInfo?.map((f2) => `${f2.area}(${f2.fit})`) });
28837
+ let garmentImage = productImage;
28838
+ if (garmentReferenceImage) {
28839
+ garmentImage = garmentReferenceImage;
28840
+ } else if (productImages && productImages.length > 1) {
28841
+ const best = await pickBestGarmentImage(productImages);
28842
+ if (best && best !== productImage) {
28843
+ console.log(`[ps-sdk:tryon] auto-picked garment reference: ${best}`);
28844
+ garmentImage = best;
28845
+ }
28846
+ }
28840
28847
  const response = await apiRef.current.submitTryOn(
28841
28848
  modelImage,
28842
- productImage,
28849
+ garmentImage,
28843
28850
  fitInfo,
28844
28851
  vtoCategory ?? "apparel",
28845
28852
  {
@@ -30326,10 +30333,29 @@ async function mount(el2) {
30326
30333
  props.sizeGuideData = fetched;
30327
30334
  }
30328
30335
  }
30336
+ let shadow;
30337
+ try {
30338
+ shadow = el2.shadowRoot ?? el2.attachShadow({ mode: "open" });
30339
+ injectStylesIntoShadow(shadow);
30340
+ } catch (err) {
30341
+ console.warn(`${TAG} shadow attach failed — falling back to direct mount`, err);
30342
+ shadow = el2;
30343
+ }
30344
+ let mountTarget;
30345
+ if (shadow instanceof ShadowRoot) {
30346
+ mountTarget = shadow.querySelector("[data-primestyle-mount]") ?? (() => {
30347
+ const c = document.createElement("div");
30348
+ c.setAttribute("data-primestyle-mount", "");
30349
+ shadow.appendChild(c);
30350
+ return c;
30351
+ })();
30352
+ } else {
30353
+ mountTarget = shadow;
30354
+ }
30329
30355
  if (props.sizeGuideData) {
30330
30356
  try {
30331
30357
  const buttonStyles = props.buttonStyles;
30332
- createSizeGuideButton(el2, props.sizeGuideData, {
30358
+ createSizeGuideButton(mountTarget, props.sizeGuideData, {
30333
30359
  accentColor: buttonStyles?.backgroundColor
30334
30360
  });
30335
30361
  console.log(`${TAG} ✓ size guide button mounted`);
@@ -30338,10 +30364,10 @@ async function mount(el2) {
30338
30364
  }
30339
30365
  }
30340
30366
  try {
30341
- const root = createRoot(el2);
30367
+ const root = createRoot(mountTarget);
30342
30368
  root.render(reactExports.createElement(PrimeStyleTryon, props));
30343
30369
  MOUNTED.set(el2, root);
30344
- console.log(`${TAG} ✓ mounted React component`);
30370
+ console.log(`${TAG} ✓ mounted React component (shadow-isolated)`);
30345
30371
  maybeFireProductView(el2);
30346
30372
  } catch (err) {
30347
30373
  console.error(`${TAG} ✗ React mount failed`, err);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shadow-DOM style collector.
3
+ *
4
+ * Vite injects all CSS imported by the storefront bundle as <style>
5
+ * tags appended to document.head at module-eval time. Theme app
6
+ * extensions render into the merchant's theme DOM, so those styles
7
+ * coexist with the merchant's theme CSS — and the theme's CSS leaks
8
+ * into our widget (table borders, button resets, font-family, etc).
9
+ *
10
+ * To isolate the widget inside a shadow root we need our SDK styles
11
+ * inside that shadow root, NOT in document.head. The cleanest way to
12
+ * intercept vite's runtime injection is to monkey-patch
13
+ * `document.head.appendChild` before our other imports evaluate, so
14
+ * every <style> tag the SDK emits during module load gets captured
15
+ * here and re-injected into each shadow root we mount later.
16
+ *
17
+ * IMPORTANT: this file MUST be imported as the *first* import of
18
+ * `src/storefront/index.ts`, before React or any CSS-importing module.
19
+ * ES module evaluation order is import-tree depth-first, so a
20
+ * side-effect import at the top of the entry runs before the rest.
21
+ */
22
+ /**
23
+ * Returns CSS text concatenated from every <style> tag captured at
24
+ * module load + any <style> tags that were already in document.head
25
+ * matching SDK selector patterns (defense-in-depth for cases where
26
+ * patching missed an injection).
27
+ */
28
+ export declare function getCollectedCss(): string;
29
+ /**
30
+ * Inject the SDK's collected styles into a shadow root so the widget
31
+ * looks identical regardless of theme. Idempotent — call once per
32
+ * shadow root after attachShadow().
33
+ */
34
+ export declare function injectStylesIntoShadow(shadow: ShadowRoot): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "5.10.103",
3
+ "version": "5.10.105",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",