@primestyleai/tryon 2.1.0 → 2.2.1

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.
@@ -45,5 +45,7 @@ export interface PrimeStyleTryonProps {
45
45
  name: string;
46
46
  values?: string[];
47
47
  }>;
48
+ /** Scan the page DOM for size guide tables/modals (like the Shopify widget does) */
49
+ scanPageForSizeGuide?: boolean;
48
50
  }
49
51
  export declare function PrimeStyleTryon(props: PrimeStyleTryonProps): import("react/jsx-runtime").JSX.Element | null;
@@ -21,6 +21,150 @@ function lsSet(key, value) {
21
21
  } catch {
22
22
  }
23
23
  }
24
+ function extractTableData(table) {
25
+ const headers = [];
26
+ const headerRow = table.querySelector("thead tr") || table.querySelector("tr");
27
+ if (!headerRow) return null;
28
+ for (const cell of headerRow.querySelectorAll("th, td")) {
29
+ const txt = (cell.textContent || "").trim();
30
+ if (txt) headers.push(txt);
31
+ }
32
+ if (headers.length < 2) return null;
33
+ const rows = [];
34
+ for (const tr of table.querySelectorAll("tbody tr, tr")) {
35
+ if (tr === headerRow) continue;
36
+ const cells = tr.querySelectorAll("td, th");
37
+ if (cells.length < 2) continue;
38
+ const row = [];
39
+ for (const c of cells) row.push((c.textContent || "").trim());
40
+ if (row.some((v) => v.length > 0)) rows.push(row);
41
+ }
42
+ return rows.length > 0 ? { headers, rows } : null;
43
+ }
44
+ const SIZE_KEYWORDS = /size|chest|bust|waist|hips|shoulder|sleeve|length|inseam|\b(XS|S|M|L|XL|XXL|2XL|3XL)\b/i;
45
+ const SG_BUTTON_KEYWORDS = /size.?(guide|chart|table)|sizing|fit.?guide|measurement/i;
46
+ function findSizeTables(container) {
47
+ const results = [];
48
+ for (const table of container.querySelectorAll("table")) {
49
+ if (SIZE_KEYWORDS.test(table.textContent || "")) {
50
+ const data = extractTableData(table);
51
+ if (data) results.push(data);
52
+ }
53
+ }
54
+ return results;
55
+ }
56
+ function detectUnit(container) {
57
+ for (const sel of [".active", ".selected", "[aria-selected=true]", "[class*=active]"]) {
58
+ try {
59
+ for (const tab of container.querySelectorAll(sel)) {
60
+ const t = (tab.textContent || "").trim().toLowerCase();
61
+ if (t.length > 20) continue;
62
+ if (/\bcm\b|centimeter/i.test(t)) return "cm";
63
+ if (/\binch|inches/i.test(t)) return "inches";
64
+ }
65
+ } catch {
66
+ }
67
+ }
68
+ for (const th of container.querySelectorAll("th")) {
69
+ const h = (th.textContent || "").toLowerCase();
70
+ if (/\(cm\)/.test(h)) return "cm";
71
+ if (/\(in|inch/.test(h)) return "inches";
72
+ }
73
+ return null;
74
+ }
75
+ function findSizeGuideButton() {
76
+ for (const el of document.querySelectorAll("button, a, [role=button], .size-guide, .size-chart, [data-action*=size], [class*=size-guide], [class*=size-chart], [class*=sizeguide], [class*=sizechart]")) {
77
+ const text = (el.textContent || "").trim();
78
+ const cls = el.className || "";
79
+ const ariaLabel = el.getAttribute("aria-label") || "";
80
+ if (SG_BUTTON_KEYWORDS.test(text) || SG_BUTTON_KEYWORDS.test(cls) || SG_BUTTON_KEYWORDS.test(ariaLabel)) {
81
+ return el;
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ function scanDomForSizeGuide() {
87
+ const containerSelectors = [
88
+ "[class*=kiwi]",
89
+ "[class*=esc-size]",
90
+ "[class*=size-matters]",
91
+ "[class*=avada-size]",
92
+ "[class*=size-guide]",
93
+ "[class*=sizeguide]",
94
+ "[class*=size-chart]",
95
+ "[class*=sizechart]",
96
+ "[class*=fit-guide]",
97
+ "[class*=fitguide]",
98
+ "[id*=size-guide]",
99
+ "[id*=sizeguide]",
100
+ "[id*=size-chart]",
101
+ "[id*=sizechart]"
102
+ ];
103
+ for (const sel of containerSelectors) {
104
+ try {
105
+ for (const el of document.querySelectorAll(sel)) {
106
+ const tables = findSizeTables(el);
107
+ if (tables.length > 0) {
108
+ return { found: true, ...tables[0], unit: detectUnit(el) };
109
+ }
110
+ }
111
+ } catch {
112
+ }
113
+ }
114
+ const pageTables = findSizeTables(document);
115
+ if (pageTables.length > 0) {
116
+ return { found: true, ...pageTables[0] };
117
+ }
118
+ const sizeBtn = findSizeGuideButton();
119
+ if (sizeBtn) {
120
+ try {
121
+ sizeBtn.click();
122
+ } catch {
123
+ }
124
+ return { found: false, buttonClicked: true };
125
+ }
126
+ return { found: false };
127
+ }
128
+ function scanAfterClick() {
129
+ const images = [];
130
+ const overlaySelectors = [
131
+ "[class*=modal]",
132
+ "[class*=drawer]",
133
+ "[class*=popup]",
134
+ "[class*=overlay]",
135
+ "[role=dialog]",
136
+ "[aria-modal=true]",
137
+ "[class*=kiwi]",
138
+ "[class*=size-guide]",
139
+ "[class*=sizeguide]",
140
+ "[class*=size-chart]",
141
+ "[class*=sizechart]"
142
+ ];
143
+ for (const sel of overlaySelectors) {
144
+ try {
145
+ for (const el of document.querySelectorAll(sel)) {
146
+ if (el.classList.contains("ps-tryon-overlay")) continue;
147
+ const style = window.getComputedStyle(el);
148
+ if (style.display === "none" || style.visibility === "hidden") continue;
149
+ const tables = findSizeTables(el);
150
+ if (tables.length > 0) {
151
+ for (const img of el.querySelectorAll("img")) {
152
+ let src = img.src || img.getAttribute("data-src") || "";
153
+ if (src.startsWith("//")) src = `https:${src}`;
154
+ const w = img.naturalWidth || img.width || 0;
155
+ const h = img.naturalHeight || img.height || 0;
156
+ if (src.startsWith("http") && (w === 0 || w > 100) && (h === 0 || h > 100)) images.push(src);
157
+ }
158
+ return { found: true, ...tables[0], unit: detectUnit(el), images };
159
+ }
160
+ }
161
+ } catch {
162
+ }
163
+ }
164
+ const pageTables = findSizeTables(document);
165
+ if (pageTables.length > 0) return { found: true, ...pageTables[0], images };
166
+ return { found: false, images };
167
+ }
24
168
  const SIZING_COUNTRIES = [
25
169
  { code: "US", label: "United States" },
26
170
  { code: "UK", label: "United Kingdom" },
@@ -185,7 +329,8 @@ function PrimeStyleTryonInner({
185
329
  productType,
186
330
  productTags,
187
331
  productVariants,
188
- productOptions
332
+ productOptions,
333
+ scanPageForSizeGuide = false
189
334
  }) {
190
335
  const [view, setView] = useState("idle");
191
336
  const [selectedFile, setSelectedFile] = useState(null);
@@ -294,27 +439,60 @@ function PrimeStyleTryonInner({
294
439
  setSizeGuide(sizeGuideProp);
295
440
  return;
296
441
  }
297
- setSizeGuideFetching(true);
298
- const baseUrl = getApiUrl(apiUrl);
299
- const key = getApiKey();
300
- const productPayload = {
301
- title: productTitle,
302
- variants: productVariants || []
303
- };
304
- if (productDescription) productPayload.description = productDescription;
305
- if (productVendor) productPayload.vendor = productVendor;
306
- if (productType) productPayload.productType = productType;
307
- if (productTags?.length) productPayload.tags = productTags;
308
- if (productOptions?.length) productPayload.options = productOptions;
309
- fetch(`${baseUrl}/api/v1/sizing/sizeguide`, {
310
- method: "POST",
311
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
312
- body: JSON.stringify({ product: productPayload })
313
- }).then((r) => r.ok ? r.json() : null).then((data) => {
314
- if (data) setSizeGuide(data);
315
- else setSizeGuide({ found: false });
316
- }).catch(() => setSizeGuide({ found: false })).finally(() => setSizeGuideFetching(false));
317
- }, [view, apiUrl, productTitle, sizeGuideProp]);
442
+ if (scanPageForSizeGuide) {
443
+ setSizeGuideFetching(true);
444
+ const domResult = scanDomForSizeGuide();
445
+ if (domResult.found && domResult.headers && domResult.rows) {
446
+ setSizeGuide({ found: true, headers: domResult.headers, rows: domResult.rows });
447
+ setSizeGuideFetching(false);
448
+ return;
449
+ }
450
+ if (domResult.buttonClicked) {
451
+ setTimeout(() => {
452
+ const afterClick = scanAfterClick();
453
+ try {
454
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
455
+ } catch {
456
+ }
457
+ try {
458
+ const closeBtn = document.querySelector("[role=dialog] button[aria-label*=close], [class*=modal] button[class*=close], [aria-modal=true] button");
459
+ if (closeBtn) closeBtn.click();
460
+ } catch {
461
+ }
462
+ if (afterClick.found && afterClick.headers && afterClick.rows) {
463
+ setSizeGuide({ found: true, headers: afterClick.headers, rows: afterClick.rows });
464
+ setSizeGuideFetching(false);
465
+ return;
466
+ }
467
+ fetchSizeGuideFromApi();
468
+ }, 800);
469
+ return;
470
+ }
471
+ }
472
+ fetchSizeGuideFromApi();
473
+ function fetchSizeGuideFromApi() {
474
+ setSizeGuideFetching(true);
475
+ const baseUrl = getApiUrl(apiUrl);
476
+ const key = getApiKey();
477
+ const productPayload = {
478
+ title: productTitle,
479
+ variants: productVariants || []
480
+ };
481
+ if (productDescription) productPayload.description = productDescription;
482
+ if (productVendor) productPayload.vendor = productVendor;
483
+ if (productType) productPayload.productType = productType;
484
+ if (productTags?.length) productPayload.tags = productTags;
485
+ if (productOptions?.length) productPayload.options = productOptions;
486
+ fetch(`${baseUrl}/api/v1/sizing/sizeguide`, {
487
+ method: "POST",
488
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
489
+ body: JSON.stringify({ product: productPayload })
490
+ }).then((r) => r.ok ? r.json() : null).then((data) => {
491
+ if (data) setSizeGuide(data);
492
+ else setSizeGuide({ found: false });
493
+ }).catch(() => setSizeGuide({ found: false })).finally(() => setSizeGuideFetching(false));
494
+ }
495
+ }, [view, apiUrl, productTitle, sizeGuideProp, scanPageForSizeGuide]);
318
496
  const stepIndex = useMemo(() => {
319
497
  switch (view) {
320
498
  case "welcome":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primestyleai/tryon",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "PrimeStyle Virtual Try-On SDK — React component & Web Component",
5
5
  "type": "module",
6
6
  "main": "dist/primestyle-tryon.js",